Skip to content

Commit b5182ad

Browse files
committed
fix: service layer typing and docs
1 parent 030a41a commit b5182ad

File tree

5 files changed

+465
-0
lines changed

5 files changed

+465
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Fix Service helper typing so dependency layers eliminate make requirements, add docgen @since for Service exports, and cover dependency elimination with dtslint checks.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Layer } from "effect"
2+
import { Effect, Service } from "effect"
3+
import { describe, expect, it } from "tstyche"
4+
5+
describe("Service public typing", () => {
6+
it("layer removes requirements satisfied by dependencies", () => {
7+
class Config extends Service<Config>()("Config", {
8+
make: (prefix: string) => Effect.succeed({ prefix })
9+
}) {}
10+
11+
class Logger extends Service<Logger>()("Logger", {
12+
make: Effect.gen(function*() {
13+
const cfg = yield* Config
14+
return { log: (msg: string) => Effect.succeed(`${cfg.prefix}:${msg}`) }
15+
}),
16+
dependencies: [Config.layer("cfg")]
17+
}) {}
18+
19+
expect(Logger.layer).type.toBe<Layer.Layer<Logger>>()
20+
expect(Logger.layerWithoutDependencies).type.toBe<Layer.Layer<Logger, never, Config>>()
21+
})
22+
23+
it("factory make keeps constructor parameters on layer", () => {
24+
class Http extends Service<Http>()("Http", {
25+
make: (base: string, timeout: number) =>
26+
Effect.succeed({
27+
base,
28+
timeout,
29+
get: (path: string) => Effect.succeed(`${base}${path}`)
30+
})
31+
}) {}
32+
33+
expect(Http.layer).type.toBe<(base: string, timeout: number) => Layer.Layer<Http, never, never>>()
34+
})
35+
})

packages/effect/src/Service.ts

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import type * as EffectTypes from "./Effect.ts"
2+
import * as Effect from "./Effect.ts"
3+
import { isEffect } from "./internal/core.ts"
4+
import * as Layer from "./Layer.ts"
5+
import type { Scope } from "./Scope.ts"
6+
import * as ServiceMap from "./ServiceMap.ts"
7+
import type * as Types from "./types/Types.ts"
8+
9+
/**
10+
* Extracts the Effect type from a make function or Effect value.
11+
*
12+
* @since 4.0.0
13+
* @category Internal
14+
*/
15+
type MakeEffect<Make> = Make extends (...args: Array<any>) => EffectTypes.Effect<any, any, any> ? ReturnType<Make>
16+
: Make
17+
18+
/**
19+
* Extracts the argument types from a make function.
20+
*
21+
* @since 4.0.0
22+
* @category Internal
23+
*/
24+
type MakeArgs<Make> = Make extends (...args: infer Args) => EffectTypes.Effect<any, any, any> ? Args : never
25+
26+
/**
27+
* Extracts the combined service requirements from dependency layers.
28+
*
29+
* @since 4.0.0
30+
* @category Internal
31+
*/
32+
type DepsContext<Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined> = Deps extends
33+
ReadonlyArray<Layer.Layer<any, any, any>> ? Layer.Services<Deps[number]>
34+
: never
35+
36+
/**
37+
* Lifts a value, Promise, or Effect into an Effect type.
38+
*
39+
* @since 4.0.0
40+
* @category Internal
41+
*/
42+
type LiftToEffect<X> = X extends EffectTypes.Effect<infer A, infer E, infer R> ? EffectTypes.Effect<A, E, R>
43+
: X extends Promise<infer A> ? EffectTypes.Effect<A, unknown>
44+
: EffectTypes.Effect<X, never>
45+
46+
/**
47+
* Layer type without dependencies - requires what make effect requires (excluding Scope).
48+
*
49+
* @since 4.0.0
50+
* @category Internal
51+
*/
52+
type LayerShapeNoDeps<Self, Eff> = Layer.Layer<
53+
Self,
54+
EffectTypes.Error<Eff>,
55+
Exclude<EffectTypes.Services<Eff>, Scope>
56+
>
57+
58+
/**
59+
* Layer type with dependencies - requires only what dependency layers require.
60+
*
61+
* @since 4.0.0
62+
* @category Internal
63+
*/
64+
type LayerShapeWithDeps<Self, Eff, DepsReq> = Layer.Layer<Self, EffectTypes.Error<Eff>, DepsReq>
65+
66+
/**
67+
* Converts an optional dependency array to a non-empty tuple type.
68+
*
69+
* @since 4.0.0
70+
* @category Internal
71+
*/
72+
type NonEmptyDeps<Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined> = Deps extends
73+
ReadonlyArray<infer L> ? readonly [L, ...Array<L>] : never
74+
75+
/**
76+
* Generates the layer type from make function, handling both factory and value cases.
77+
*
78+
* @since 4.0.0
79+
* @category Internal
80+
*/
81+
type LayerFromMake<Self, Make, Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined> = Deps extends
82+
undefined ? ([MakeArgs<Make>] extends [never] ? LayerShapeNoDeps<Self, MakeEffect<Make>>
83+
: (...args: MakeArgs<Make>) => LayerShapeNoDeps<Self, MakeEffect<Make>>)
84+
: ([MakeArgs<Make>] extends [never] ? LayerShapeWithDeps<Self, MakeEffect<Make>, DepsContext<Deps>>
85+
: (...args: MakeArgs<Make>) => LayerShapeWithDeps<Self, MakeEffect<Make>, DepsContext<Deps>>)
86+
87+
/**
88+
* Layer type ignoring dependencies - always requires what make effect requires.
89+
*
90+
* @since 4.0.0
91+
* @category Internal
92+
*/
93+
type LayerWithoutDepsFromMake<Self, Make> = [MakeArgs<Make>] extends [never] ? LayerShapeNoDeps<Self, MakeEffect<Make>>
94+
: (...args: MakeArgs<Make>) => LayerShapeNoDeps<Self, MakeEffect<Make>>
95+
96+
// Type guard to check if a value is a Promise.
97+
const isPromise = (u: unknown): u is Promise<unknown> =>
98+
typeof u === "object" && u !== null && "then" in u && typeof (u as any).then === "function"
99+
100+
// Builds the `use` helper for a service, allowing callback-based access.
101+
const buildUse = (service: any) => {
102+
return <X>(f: (svc: any) => X): EffectTypes.Effect<any, any, any> =>
103+
Effect.gen(function*() {
104+
const svc = yield* service
105+
const result = f(svc)
106+
if (isEffect(result)) {
107+
return yield* result
108+
}
109+
if (isPromise(result)) {
110+
return yield* Effect.promise(() => result)
111+
}
112+
return result
113+
})
114+
}
115+
116+
// Builds the `layer` or `layerWithoutDependencies`, handling factories and dependency provision.
117+
const buildLayer = (
118+
service: any,
119+
make: EffectTypes.Effect<any, any, any> | ((...args: Array<any>) => EffectTypes.Effect<any, any, any>),
120+
dependencies?: ReadonlyArray<Layer.Layer<any, any, any>>
121+
) => {
122+
const isFactory = typeof make === "function"
123+
const depsLayer = dependencies && dependencies.length > 0
124+
? Layer.mergeAll(...(dependencies as NonEmptyDeps<typeof dependencies>))
125+
: undefined
126+
127+
const base = (...args: Array<any>) => {
128+
const eff = isFactory ? (make as any)(...args) : make
129+
return Layer.effect(service, eff)
130+
}
131+
132+
return depsLayer
133+
? isFactory
134+
? (...args: Array<any>) => Layer.provide(base(...args), depsLayer)
135+
: Layer.provide(base(), depsLayer)
136+
: isFactory
137+
? (...args: Array<any>) => base(...args)
138+
: base()
139+
}
140+
141+
/**
142+
* Extended ServiceClass with layer helpers for services with `make`.
143+
*
144+
* Provides:
145+
* - `make`: The effect or factory function to create the service
146+
* - `use`: Callback-based service access
147+
* - `layer`: Layer constructor respecting dependencies
148+
* - `layerWithoutDependencies`: Layer constructor ignoring dependencies (only when deps provided)
149+
*
150+
* @since 4.0.0
151+
* @category Models
152+
*/
153+
export type ServiceWithMake<
154+
Self,
155+
Id extends string,
156+
Shape,
157+
Make extends EffectTypes.Effect<any, any, any> | ((...args: any) => EffectTypes.Effect<any, any, any>),
158+
Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined
159+
> = ServiceMap.ServiceClass<Self, Id, Shape> & {
160+
readonly make: Make
161+
readonly use: <X>(f: (svc: Shape) => X) => LiftToEffect<X>
162+
readonly layer: LayerFromMake<Self, Make, Deps>
163+
readonly layerWithoutDependencies: Deps extends undefined ? never : LayerWithoutDepsFromMake<Self, Make>
164+
}
165+
166+
/**
167+
* Creates a service with layer helpers when `make` is provided.
168+
*
169+
* @example
170+
* ```ts
171+
* import { Service, Effect } from "effect"
172+
*
173+
* class Logger extends Service<Logger>()("Logger", {
174+
* make: Effect.sync(() => ({ log: (msg: string) => console.log(msg) }))
175+
* }) {}
176+
*
177+
* // Use Logger.layer, Logger.use, etc.
178+
* ```
179+
*
180+
* @since 4.0.0
181+
* @category Constructors
182+
*/
183+
export type ServiceConstructor = {
184+
// Plain tag (no make)
185+
<Identifier, Shape = Identifier>(key: string): ServiceMap.Service<Identifier, Shape>
186+
// Curried with explicit Shape; make optional
187+
<Self, Shape>(): <
188+
const Identifier extends string,
189+
E,
190+
R = Types.unassigned,
191+
Args extends ReadonlyArray<any> = never,
192+
Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined = undefined
193+
>(
194+
id: Identifier,
195+
options?: {
196+
readonly make?: ((...args: Args) => EffectTypes.Effect<Shape, E, R>) | EffectTypes.Effect<Shape, E, R> | undefined
197+
readonly dependencies?: Deps
198+
} | undefined
199+
) => [Types.unassigned] extends [R] ? ServiceMap.ServiceClass<Self, Identifier, Shape>
200+
: ServiceWithMake<
201+
Self,
202+
Identifier,
203+
Shape,
204+
[Args] extends [never] ? EffectTypes.Effect<Shape, E, R> : (...args: Args) => EffectTypes.Effect<Shape, E, R>,
205+
Deps
206+
>
207+
// Curried with inferred Shape; make required
208+
<Self>(): <
209+
const Identifier extends string,
210+
Make extends EffectTypes.Effect<any, any, any> | ((...args: any) => EffectTypes.Effect<any, any, any>),
211+
Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined = undefined
212+
>(
213+
id: Identifier,
214+
options: {
215+
readonly make: Make
216+
readonly dependencies?: Deps
217+
}
218+
) => ServiceWithMake<
219+
Self,
220+
Identifier,
221+
Make extends
222+
| EffectTypes.Effect<infer _A, infer _E, infer _R>
223+
| ((...args: infer _Args) => EffectTypes.Effect<infer _A, infer _E, infer _R>) ? _A
224+
: never,
225+
Make,
226+
Deps
227+
>
228+
}
229+
230+
const ServiceImpl = (...args: Array<any>) => {
231+
if (args.length === 0) {
232+
const baseService = ServiceMap.Service()
233+
234+
return function(key: string, options?: {
235+
readonly make?: any
236+
readonly dependencies?: ReadonlyArray<Layer.Layer<any, any, any>>
237+
}) {
238+
const service = options?.make
239+
? baseService(key, { make: options.make })
240+
: baseService(key)
241+
242+
if (options?.make) {
243+
const deps = options.dependencies
244+
type Self = typeof service
245+
type Make = typeof options.make
246+
type Deps = typeof deps
247+
248+
const svc = service as Types.Mutable<
249+
& ServiceMap.ServiceClass<Self, typeof service.key, typeof service.Service>
250+
& Partial<ServiceWithMake<Self, typeof service.key, typeof service.Service, Make, Deps>>
251+
>
252+
253+
svc.layer = buildLayer(svc, options.make, deps) as LayerFromMake<Self, Make, Deps>
254+
if (deps && deps.length > 0) {
255+
svc.layerWithoutDependencies = buildLayer(svc, options.make) as LayerWithoutDepsFromMake<Self, Make>
256+
}
257+
svc.use = buildUse(svc) as ServiceWithMake<Self, typeof service.key, typeof service.Service, Make, Deps>["use"]
258+
}
259+
260+
return service
261+
}
262+
}
263+
264+
return (ServiceMap.Service as (...fnArgs: Array<any>) => any)(...args)
265+
}
266+
267+
/**
268+
* Layer-aware service constructor. Use this when providing `make` to get
269+
* automatic `layer`, `layerWithoutDependencies`, and `use` helpers.
270+
*
271+
* @example
272+
* ```ts
273+
* import { Service, Effect } from "effect"
274+
*
275+
* class Logger extends Service<Logger>()("Logger", {
276+
* make: Effect.sync(() => ({ log: (msg: string) => console.log(msg) }))
277+
* }) {}
278+
*
279+
* // Use Logger.layer, Logger.use, etc.
280+
* ```
281+
*
282+
* @since 4.0.0
283+
*/
284+
export const Service = ServiceImpl as ServiceConstructor
285+
286+
/**
287+
* Alias to the underlying service tag type.
288+
*
289+
* @since 4.0.0
290+
*/
291+
export type ServiceTag<Identifier, Shape = Identifier> = ServiceMap.Service<Identifier, Shape>
292+
293+
/**
294+
* Alias to the underlying service class type.
295+
*
296+
* @since 4.0.0
297+
*/
298+
export type ServiceClass<Self, Identifier extends string, Shape> = ServiceMap.ServiceClass<Self, Identifier, Shape>

packages/effect/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,17 @@ export * as ScopedRef from "./ScopedRef.ts"
12981298
*
12991299
* @since 4.0.0
13001300
*/
1301+
export {
1302+
/**
1303+
* Layer-aware service constructor with automatic helpers.
1304+
*
1305+
* @since 4.0.0
1306+
*/
1307+
Service
1308+
} from "./Service.ts"
1309+
/**
1310+
* @since 4.0.0
1311+
*/
13011312
export * as ServiceMap from "./ServiceMap.ts"
13021313

13031314
/**

0 commit comments

Comments
 (0)