Skip to content

Commit aeab349

Browse files
Add Effect.Service to Context.Tag with Layer refactor (#651)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d6305ad commit aeab349

11 files changed

Lines changed: 1149 additions & 1 deletion
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@effect/language-service": minor
3+
---
4+
5+
Add refactor to convert `Effect.Service` to `Context.Tag` with a static `Layer` property.
6+
7+
Supports all combinator kinds (`effect`, `scoped`, `sync`, `succeed`) and `dependencies`. The refactor replaces the `Effect.Service` class declaration with a `Context.Tag` class that has a `static layer` property using the corresponding `Layer` combinator.
8+
9+
Before:
10+
```ts
11+
export class MyService extends Effect.Service<MyService>()("MyService", {
12+
effect: Effect.gen(function*() {
13+
return { value: "hello" }
14+
})
15+
}){}
16+
```
17+
18+
After:
19+
```ts
20+
export class MyService extends Context.Tag("MyService")<MyService, { value: string }>() {
21+
static layer = Layer.effect(this, Effect.gen(function*() {
22+
return { value: "hello" }
23+
}));
24+
}
25+
```

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ And you're done! You'll now be able to use a set of refactors and diagnostics th
112112
- Toggle between pipe styles `X.pipe(Y)` and `pipe(X, Y)`
113113
- Layer Magic: Automatically compose and build layers based on service dependencies
114114
- Structural Type to Schema: Convert TypeScript interfaces and type aliases to Effect Schema classes, with automatic detection and reuse of existing schemas
115+
- Convert `Effect.Service` to `Context.Tag` with a static `Layer` property (supports `effect`, `scoped`, `sync`, `succeed` combinators and `dependencies`)
115116
116117
### Codegens
117118
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Result of running refactor effectServiceToClassWithLayer at position 100:20
2+
import { Effect, Context } from "effect"
3+
import * as Layer from "effect/Layer"
4+
5+
// this can be converted to a Context.Tag with a static layer property
6+
export class MyService extends Effect.Service<MyService>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyService", {
7+
effect: Effect.gen(function*() {
8+
return {
9+
value: "MyService"
10+
}
11+
})
12+
}){}
13+
14+
// example result
15+
export class MyServiceAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceAsContextTag")<MyServiceAsContextTag, {
16+
value: string
17+
}>(){
18+
static layer = Layer.effect(this, Effect.gen(function*(){
19+
return {
20+
value: "MyService"
21+
}
22+
}))
23+
}
24+
25+
export class MyServiceWithArgs extends Effect.Service<MyServiceWithArgs>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgs", {
26+
effect: (arg: string) => Effect.gen(function*(){
27+
return {
28+
value: arg
29+
}
30+
})
31+
}){}
32+
33+
// example result
34+
export class MyServiceWithArgsAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsAsContextTag")<MyServiceWithArgsAsContextTag, {
35+
value: string
36+
}>(){
37+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*(){
38+
return {
39+
value: arg
40+
}
41+
}))
42+
}
43+
44+
// there is also a scoped variant, which should behave the same as the effect variant, but uses the scoped combinator.
45+
export class MyServiceWithArgsScoped extends Effect.Service<MyServiceWithArgsScoped>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScoped", {
46+
scoped: (arg: string) => Effect.gen(function*(){
47+
return {
48+
value: arg
49+
}
50+
})
51+
}){}
52+
53+
export class MyServiceWithArgsScopedAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAsContextTag")<MyServiceWithArgsScopedAsContextTag, {
54+
value: string
55+
}>(){
56+
static layer = (arg: string) => Layer.scoped(this, Effect.gen(function*(){
57+
return {
58+
value: arg
59+
}
60+
}))
61+
}
62+
63+
// the sync variant returns the structure directly, without using an intermediate effect
64+
export class MyServiceSync extends Effect.Service<MyServiceSync>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSync", {
65+
sync: () => {
66+
return {
67+
value: "MyService"
68+
}
69+
}
70+
}){}
71+
72+
// example result
73+
export class MyServiceSyncAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSyncAsContextTag")<MyServiceSyncAsContextTag, {
74+
value: string
75+
}>(){
76+
static layer = Layer.sync(this, () => {
77+
return {
78+
value: "MyService"
79+
}
80+
})
81+
}
82+
83+
// the succeed variant returns the structure directly, without using an intermediate effect
84+
export class MyServiceSucceed extends Effect.Service<MyServiceSucceed>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSucceed", {
85+
succeed: {
86+
value: "MyService"
87+
}
88+
}){}
89+
90+
// example result
91+
export class MyServiceSucceedAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSucceedAsContextTag")<MyServiceSucceedAsContextTag, {
92+
value: "MyService"
93+
}>(){
94+
static layer = Layer.succeed(this, {
95+
value: "MyService"
96+
})
97+
}
98+
99+
// all variants support dependencies as well
100+
export class MyServiceWithArgsScopedAndDependencies extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAndDependencies")<MyServiceWithArgsScopedAndDependencies, { value: string }>() {
101+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*() {
102+
return {
103+
value: arg
104+
}
105+
})).pipe(Layer.provide(Layer.mergeAll(MyServiceWithArgsScoped.Default("hello"))));
106+
}
107+
108+
// example result
109+
export class MyServiceWithArgsScopedAndDependenciesAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAndDependenciesAsContextTag")<MyServiceWithArgsScopedAndDependenciesAsContextTag, {
110+
value: string
111+
}>(){
112+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*(){
113+
return {
114+
value: arg
115+
}
116+
})).pipe(
117+
Layer.provide(Layer.mergeAll(MyServiceWithArgsScoped.Default("hello")))
118+
)
119+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Result of running refactor effectServiceToClassWithLayer at position 25:20
2+
import { Effect, Context } from "effect"
3+
import * as Layer from "effect/Layer"
4+
5+
// this can be converted to a Context.Tag with a static layer property
6+
export class MyService extends Effect.Service<MyService>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyService", {
7+
effect: Effect.gen(function*() {
8+
return {
9+
value: "MyService"
10+
}
11+
})
12+
}){}
13+
14+
// example result
15+
export class MyServiceAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceAsContextTag")<MyServiceAsContextTag, {
16+
value: string
17+
}>(){
18+
static layer = Layer.effect(this, Effect.gen(function*(){
19+
return {
20+
value: "MyService"
21+
}
22+
}))
23+
}
24+
25+
export class MyServiceWithArgs extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgs")<MyServiceWithArgs, { value: string }>() {
26+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*() {
27+
return {
28+
value: arg
29+
}
30+
}));
31+
}
32+
33+
// example result
34+
export class MyServiceWithArgsAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsAsContextTag")<MyServiceWithArgsAsContextTag, {
35+
value: string
36+
}>(){
37+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*(){
38+
return {
39+
value: arg
40+
}
41+
}))
42+
}
43+
44+
// there is also a scoped variant, which should behave the same as the effect variant, but uses the scoped combinator.
45+
export class MyServiceWithArgsScoped extends Effect.Service<MyServiceWithArgsScoped>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScoped", {
46+
scoped: (arg: string) => Effect.gen(function*(){
47+
return {
48+
value: arg
49+
}
50+
})
51+
}){}
52+
53+
export class MyServiceWithArgsScopedAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAsContextTag")<MyServiceWithArgsScopedAsContextTag, {
54+
value: string
55+
}>(){
56+
static layer = (arg: string) => Layer.scoped(this, Effect.gen(function*(){
57+
return {
58+
value: arg
59+
}
60+
}))
61+
}
62+
63+
// the sync variant returns the structure directly, without using an intermediate effect
64+
export class MyServiceSync extends Effect.Service<MyServiceSync>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSync", {
65+
sync: () => {
66+
return {
67+
value: "MyService"
68+
}
69+
}
70+
}){}
71+
72+
// example result
73+
export class MyServiceSyncAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSyncAsContextTag")<MyServiceSyncAsContextTag, {
74+
value: string
75+
}>(){
76+
static layer = Layer.sync(this, () => {
77+
return {
78+
value: "MyService"
79+
}
80+
})
81+
}
82+
83+
// the succeed variant returns the structure directly, without using an intermediate effect
84+
export class MyServiceSucceed extends Effect.Service<MyServiceSucceed>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSucceed", {
85+
succeed: {
86+
value: "MyService"
87+
}
88+
}){}
89+
90+
// example result
91+
export class MyServiceSucceedAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSucceedAsContextTag")<MyServiceSucceedAsContextTag, {
92+
value: "MyService"
93+
}>(){
94+
static layer = Layer.succeed(this, {
95+
value: "MyService"
96+
})
97+
}
98+
99+
// all variants support dependencies as well
100+
export class MyServiceWithArgsScopedAndDependencies extends Effect.Service<MyServiceWithArgsScopedAndDependencies>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAndDependencies", {
101+
effect: (arg: string) => Effect.gen(function*(){
102+
return {
103+
value: arg
104+
}
105+
}),
106+
dependencies: [MyServiceWithArgsScoped.Default("hello")]
107+
}){}
108+
109+
// example result
110+
export class MyServiceWithArgsScopedAndDependenciesAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAndDependenciesAsContextTag")<MyServiceWithArgsScopedAndDependenciesAsContextTag, {
111+
value: string
112+
}>(){
113+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*(){
114+
return {
115+
value: arg
116+
}
117+
})).pipe(
118+
Layer.provide(Layer.mergeAll(MyServiceWithArgsScoped.Default("hello")))
119+
)
120+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Result of running refactor effectServiceToClassWithLayer at position 45:20
2+
import { Effect, Context } from "effect"
3+
import * as Layer from "effect/Layer"
4+
5+
// this can be converted to a Context.Tag with a static layer property
6+
export class MyService extends Effect.Service<MyService>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyService", {
7+
effect: Effect.gen(function*() {
8+
return {
9+
value: "MyService"
10+
}
11+
})
12+
}){}
13+
14+
// example result
15+
export class MyServiceAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceAsContextTag")<MyServiceAsContextTag, {
16+
value: string
17+
}>(){
18+
static layer = Layer.effect(this, Effect.gen(function*(){
19+
return {
20+
value: "MyService"
21+
}
22+
}))
23+
}
24+
25+
export class MyServiceWithArgs extends Effect.Service<MyServiceWithArgs>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgs", {
26+
effect: (arg: string) => Effect.gen(function*(){
27+
return {
28+
value: arg
29+
}
30+
})
31+
}){}
32+
33+
// example result
34+
export class MyServiceWithArgsAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsAsContextTag")<MyServiceWithArgsAsContextTag, {
35+
value: string
36+
}>(){
37+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*(){
38+
return {
39+
value: arg
40+
}
41+
}))
42+
}
43+
44+
// there is also a scoped variant, which should behave the same as the effect variant, but uses the scoped combinator.
45+
export class MyServiceWithArgsScoped extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScoped")<MyServiceWithArgsScoped, { value: string }>() {
46+
static layer = (arg: string) => Layer.scoped(this, Effect.gen(function*() {
47+
return {
48+
value: arg
49+
}
50+
}));
51+
}
52+
53+
export class MyServiceWithArgsScopedAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAsContextTag")<MyServiceWithArgsScopedAsContextTag, {
54+
value: string
55+
}>(){
56+
static layer = (arg: string) => Layer.scoped(this, Effect.gen(function*(){
57+
return {
58+
value: arg
59+
}
60+
}))
61+
}
62+
63+
// the sync variant returns the structure directly, without using an intermediate effect
64+
export class MyServiceSync extends Effect.Service<MyServiceSync>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSync", {
65+
sync: () => {
66+
return {
67+
value: "MyService"
68+
}
69+
}
70+
}){}
71+
72+
// example result
73+
export class MyServiceSyncAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSyncAsContextTag")<MyServiceSyncAsContextTag, {
74+
value: string
75+
}>(){
76+
static layer = Layer.sync(this, () => {
77+
return {
78+
value: "MyService"
79+
}
80+
})
81+
}
82+
83+
// the succeed variant returns the structure directly, without using an intermediate effect
84+
export class MyServiceSucceed extends Effect.Service<MyServiceSucceed>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSucceed", {
85+
succeed: {
86+
value: "MyService"
87+
}
88+
}){}
89+
90+
// example result
91+
export class MyServiceSucceedAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceSucceedAsContextTag")<MyServiceSucceedAsContextTag, {
92+
value: "MyService"
93+
}>(){
94+
static layer = Layer.succeed(this, {
95+
value: "MyService"
96+
})
97+
}
98+
99+
// all variants support dependencies as well
100+
export class MyServiceWithArgsScopedAndDependencies extends Effect.Service<MyServiceWithArgsScopedAndDependencies>()("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAndDependencies", {
101+
effect: (arg: string) => Effect.gen(function*(){
102+
return {
103+
value: arg
104+
}
105+
}),
106+
dependencies: [MyServiceWithArgsScoped.Default("hello")]
107+
}){}
108+
109+
// example result
110+
export class MyServiceWithArgsScopedAndDependenciesAsContextTag extends Context.Tag("@effect/harness-effect-v3/examples/refactors/effectServiceToClassWithLayer/MyServiceWithArgsScopedAndDependenciesAsContextTag")<MyServiceWithArgsScopedAndDependenciesAsContextTag, {
111+
value: string
112+
}>(){
113+
static layer = (arg: string) => Layer.effect(this, Effect.gen(function*(){
114+
return {
115+
value: arg
116+
}
117+
})).pipe(
118+
Layer.provide(Layer.mergeAll(MyServiceWithArgsScoped.Default("hello")))
119+
)
120+
}

0 commit comments

Comments
 (0)