Skip to content

Commit 5f07f8e

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

File tree

5 files changed

+403
-0
lines changed

5 files changed

+403
-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: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/**
2+
* Layer-aware service constructor. Use this when providing `make` to get
3+
* automatic `layer`, `layerWithoutDependencies`, and `use` helpers.
4+
*
5+
* @since 4.0.0
6+
* @category Constructors
7+
*/
8+
import type * as EffectTypes from "./Effect.ts"
9+
import * as Effect from "./Effect.ts"
10+
import { isEffect } from "./internal/core.ts"
11+
import * as Layer from "./Layer.ts"
12+
import type { Scope } from "./Scope.ts"
13+
import * as ServiceMap from "./ServiceMap.ts"
14+
import type * as Types from "./types/Types.ts"
15+
16+
// Internal helper types for layer generation
17+
type MakeEffect<Make> = Make extends (...args: Array<any>) => EffectTypes.Effect<any, any, any> ? ReturnType<Make>
18+
: Make
19+
type MakeArgs<Make> = Make extends (...args: infer Args) => EffectTypes.Effect<any, any, any> ? Args : never
20+
21+
type DepsContext<Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined> = Deps extends
22+
ReadonlyArray<Layer.Layer<any, any, any>> ? Layer.Services<Deps[number]>
23+
: never
24+
25+
type LiftToEffect<X> = X extends EffectTypes.Effect<infer A, infer E, infer R> ? EffectTypes.Effect<A, E, R>
26+
: X extends Promise<infer A> ? EffectTypes.Effect<A, unknown>
27+
: EffectTypes.Effect<X, never>
28+
29+
// Without dependencies: layer requires what make requires
30+
type LayerShapeNoDeps<Self, Eff> = Layer.Layer<
31+
Self,
32+
EffectTypes.Error<Eff>,
33+
Exclude<EffectTypes.Services<Eff>, Scope>
34+
>
35+
36+
// With dependencies: layer requires ONLY what dependencies require (make's reqs are satisfied)
37+
type LayerShapeWithDeps<Self, Eff, DepsReq> = Layer.Layer<Self, EffectTypes.Error<Eff>, DepsReq>
38+
39+
type NonEmptyDeps<Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined> = Deps extends
40+
ReadonlyArray<infer L> ? readonly [L, ...Array<L>] : never
41+
42+
type LayerFromMake<Self, Make, Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined> = Deps extends
43+
undefined ? ([MakeArgs<Make>] extends [never] ? LayerShapeNoDeps<Self, MakeEffect<Make>>
44+
: (...args: MakeArgs<Make>) => LayerShapeNoDeps<Self, MakeEffect<Make>>)
45+
: ([MakeArgs<Make>] extends [never] ? LayerShapeWithDeps<Self, MakeEffect<Make>, DepsContext<Deps>>
46+
: (...args: MakeArgs<Make>) => LayerShapeWithDeps<Self, MakeEffect<Make>, DepsContext<Deps>>)
47+
48+
type LayerWithoutDepsFromMake<Self, Make> = [MakeArgs<Make>] extends [never] ? LayerShapeNoDeps<Self, MakeEffect<Make>>
49+
: (...args: MakeArgs<Make>) => LayerShapeNoDeps<Self, MakeEffect<Make>>
50+
51+
// Type simplification helper to improve IDE display
52+
// Forces TypeScript to expand intermediate type aliases like LayerShapeNoDeps to Layer.Layer<...>
53+
const isPromise = (u: unknown): u is Promise<unknown> =>
54+
typeof u === "object" && u !== null && "then" in u && typeof (u as any).then === "function"
55+
56+
const buildUse = (service: any) => {
57+
return <X>(f: (svc: any) => X): EffectTypes.Effect<any, any, any> =>
58+
Effect.gen(function*() {
59+
const svc = yield* service
60+
const result = f(svc)
61+
if (isEffect(result)) {
62+
return yield* result
63+
}
64+
if (isPromise(result)) {
65+
return yield* Effect.promise(() => result)
66+
}
67+
return result
68+
})
69+
}
70+
71+
const buildLayer = (
72+
service: any,
73+
make: EffectTypes.Effect<any, any, any> | ((...args: Array<any>) => EffectTypes.Effect<any, any, any>),
74+
dependencies?: ReadonlyArray<Layer.Layer<any, any, any>>
75+
) => {
76+
const isFactory = typeof make === "function"
77+
const depsLayer = dependencies && dependencies.length > 0
78+
? Layer.mergeAll(...(dependencies as NonEmptyDeps<typeof dependencies>))
79+
: undefined
80+
81+
const base = (...args: Array<any>) => {
82+
const eff = isFactory ? (make as any)(...args) : make
83+
return Layer.effect(service, eff)
84+
}
85+
86+
return depsLayer
87+
? isFactory
88+
? (...args: Array<any>) => Layer.provide(base(...args), depsLayer)
89+
: Layer.provide(base(), depsLayer)
90+
: isFactory
91+
? (...args: Array<any>) => base(...args)
92+
: base()
93+
}
94+
95+
/**
96+
* Extended ServiceClass with layer helpers for services with `make`.
97+
*
98+
* @since 4.0.0
99+
* @category Models
100+
*/
101+
export type ServiceWithMake<
102+
Self,
103+
Id extends string,
104+
Shape,
105+
Make extends EffectTypes.Effect<any, any, any> | ((...args: any) => EffectTypes.Effect<any, any, any>),
106+
Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined
107+
> = ServiceMap.ServiceClass<Self, Id, Shape> & {
108+
readonly make: Make
109+
readonly use: <X>(f: (svc: Shape) => X) => LiftToEffect<X>
110+
readonly layer: [MakeArgs<Make>] extends [never]
111+
? Deps extends undefined
112+
? Layer.Layer<Self, EffectTypes.Error<MakeEffect<Make>>, Exclude<EffectTypes.Services<MakeEffect<Make>>, Scope>>
113+
: Layer.Layer<Self, EffectTypes.Error<MakeEffect<Make>>, DepsContext<Deps>>
114+
: Deps extends undefined ? (
115+
...args: MakeArgs<Make>
116+
) => Layer.Layer<
117+
Self,
118+
EffectTypes.Error<MakeEffect<Make>>,
119+
Exclude<EffectTypes.Services<MakeEffect<Make>>, Scope>
120+
>
121+
: (...args: MakeArgs<Make>) => Layer.Layer<Self, EffectTypes.Error<MakeEffect<Make>>, DepsContext<Deps>>
122+
readonly layerWithoutDependencies: Deps extends undefined ? never
123+
: [MakeArgs<Make>] extends [never]
124+
? Layer.Layer<Self, EffectTypes.Error<MakeEffect<Make>>, Exclude<EffectTypes.Services<MakeEffect<Make>>, Scope>>
125+
: (
126+
...args: MakeArgs<Make>
127+
) => Layer.Layer<Self, EffectTypes.Error<MakeEffect<Make>>, Exclude<EffectTypes.Services<MakeEffect<Make>>, Scope>>
128+
}
129+
130+
/**
131+
* Creates a service with layer helpers when `make` is provided.
132+
*
133+
* @example
134+
* ```ts
135+
* import { Service, Effect } from "effect"
136+
*
137+
* class Logger extends Service<Logger>()("Logger", {
138+
* make: Effect.sync(() => ({ log: (msg: string) => console.log(msg) }))
139+
* }) {}
140+
*
141+
* // Use Logger.layer, Logger.use, etc.
142+
* ```
143+
*
144+
* @since 4.0.0
145+
* @category Constructors
146+
*/
147+
type ServiceFactory = {
148+
// Plain tag (no make)
149+
<Identifier, Shape = Identifier>(key: string): ServiceMap.Service<Identifier, Shape>
150+
// Curried with explicit Shape; make optional
151+
<Self, Shape>(): <
152+
const Identifier extends string,
153+
E,
154+
R = Types.unassigned,
155+
Args extends ReadonlyArray<any> = never,
156+
Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined = undefined
157+
>(
158+
id: Identifier,
159+
options?: {
160+
readonly make?: ((...args: Args) => EffectTypes.Effect<Shape, E, R>) | EffectTypes.Effect<Shape, E, R> | undefined
161+
readonly dependencies?: Deps
162+
} | undefined
163+
) => [Types.unassigned] extends [R] ? ServiceMap.ServiceClass<Self, Identifier, Shape>
164+
: ServiceWithMake<
165+
Self,
166+
Identifier,
167+
Shape,
168+
[Args] extends [never] ? EffectTypes.Effect<Shape, E, R> : (...args: Args) => EffectTypes.Effect<Shape, E, R>,
169+
Deps
170+
>
171+
// Curried with inferred Shape; make required
172+
<Self>(): <
173+
const Identifier extends string,
174+
Make extends EffectTypes.Effect<any, any, any> | ((...args: any) => EffectTypes.Effect<any, any, any>),
175+
Deps extends ReadonlyArray<Layer.Layer<any, any, any>> | undefined = undefined
176+
>(
177+
id: Identifier,
178+
options: {
179+
readonly make: Make
180+
readonly dependencies?: Deps
181+
}
182+
) => ServiceWithMake<
183+
Self,
184+
Identifier,
185+
Make extends
186+
| EffectTypes.Effect<infer _A, infer _E, infer _R>
187+
| ((...args: infer _Args) => EffectTypes.Effect<infer _A, infer _E, infer _R>) ? _A
188+
: never,
189+
Make,
190+
Deps
191+
>
192+
}
193+
194+
const ServiceImpl = (...args: Array<any>) => {
195+
if (args.length === 0) {
196+
const baseService = ServiceMap.Service()
197+
198+
return function(key: string, options?: {
199+
readonly make?: any
200+
readonly dependencies?: ReadonlyArray<Layer.Layer<any, any, any>>
201+
}) {
202+
const service = options?.make
203+
? baseService(key, { make: options.make })
204+
: baseService(key)
205+
206+
if (options?.make) {
207+
const deps = options.dependencies
208+
type Self = typeof service
209+
type Make = typeof options.make
210+
type Deps = typeof deps
211+
212+
const svc = service as Types.Mutable<
213+
& ServiceMap.ServiceClass<Self, typeof service.key, typeof service.Service>
214+
& Partial<ServiceWithMake<Self, typeof service.key, typeof service.Service, Make, Deps>>
215+
>
216+
217+
svc.layer = buildLayer(svc, options.make, deps) as LayerFromMake<Self, Make, Deps>
218+
if (deps && deps.length > 0) {
219+
svc.layerWithoutDependencies = buildLayer(svc, options.make) as LayerWithoutDepsFromMake<Self, Make>
220+
}
221+
svc.use = buildUse(svc) as ServiceWithMake<Self, typeof service.key, typeof service.Service, Make, Deps>["use"]
222+
}
223+
224+
return service
225+
}
226+
}
227+
228+
return (ServiceMap.Service as (...fnArgs: Array<any>) => any)(...args)
229+
}
230+
231+
/**
232+
* Layer-aware service constructor with generated `layer`, `layerWithoutDependencies`, and `use` helpers.
233+
*
234+
* @since 4.0.0
235+
*/
236+
export const Service = ServiceImpl as ServiceFactory

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)