@dojocho/effect-ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/DOJO.md +22 -0
  2. package/dojo.json +50 -0
  3. package/katas/001-hello-effect/SENSEI.md +72 -0
  4. package/katas/001-hello-effect/solution.test.ts +35 -0
  5. package/katas/001-hello-effect/solution.ts +16 -0
  6. package/katas/002-transform-with-map/SENSEI.md +72 -0
  7. package/katas/002-transform-with-map/solution.test.ts +33 -0
  8. package/katas/002-transform-with-map/solution.ts +16 -0
  9. package/katas/003-generator-pipelines/SENSEI.md +72 -0
  10. package/katas/003-generator-pipelines/solution.test.ts +40 -0
  11. package/katas/003-generator-pipelines/solution.ts +29 -0
  12. package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
  13. package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
  14. package/katas/004-flatmap-and-chaining/solution.ts +18 -0
  15. package/katas/005-pipe-composition/SENSEI.md +81 -0
  16. package/katas/005-pipe-composition/solution.test.ts +41 -0
  17. package/katas/005-pipe-composition/solution.ts +19 -0
  18. package/katas/006-handle-errors/SENSEI.md +86 -0
  19. package/katas/006-handle-errors/solution.test.ts +53 -0
  20. package/katas/006-handle-errors/solution.ts +30 -0
  21. package/katas/007-tagged-errors/SENSEI.md +79 -0
  22. package/katas/007-tagged-errors/solution.test.ts +82 -0
  23. package/katas/007-tagged-errors/solution.ts +37 -0
  24. package/katas/008-error-patterns/SENSEI.md +89 -0
  25. package/katas/008-error-patterns/solution.test.ts +41 -0
  26. package/katas/008-error-patterns/solution.ts +38 -0
  27. package/katas/009-option-type/SENSEI.md +96 -0
  28. package/katas/009-option-type/solution.test.ts +49 -0
  29. package/katas/009-option-type/solution.ts +26 -0
  30. package/katas/010-either-and-exit/SENSEI.md +86 -0
  31. package/katas/010-either-and-exit/solution.test.ts +33 -0
  32. package/katas/010-either-and-exit/solution.ts +17 -0
  33. package/katas/011-services-and-context/SENSEI.md +82 -0
  34. package/katas/011-services-and-context/solution.test.ts +23 -0
  35. package/katas/011-services-and-context/solution.ts +17 -0
  36. package/katas/012-layers/SENSEI.md +73 -0
  37. package/katas/012-layers/solution.test.ts +23 -0
  38. package/katas/012-layers/solution.ts +26 -0
  39. package/katas/013-testing-effects/SENSEI.md +88 -0
  40. package/katas/013-testing-effects/solution.test.ts +41 -0
  41. package/katas/013-testing-effects/solution.ts +20 -0
  42. package/katas/014-schema-basics/SENSEI.md +81 -0
  43. package/katas/014-schema-basics/solution.test.ts +35 -0
  44. package/katas/014-schema-basics/solution.ts +25 -0
  45. package/katas/015-domain-modeling/SENSEI.md +85 -0
  46. package/katas/015-domain-modeling/solution.test.ts +46 -0
  47. package/katas/015-domain-modeling/solution.ts +42 -0
  48. package/katas/016-retry-and-schedule/SENSEI.md +72 -0
  49. package/katas/016-retry-and-schedule/solution.test.ts +26 -0
  50. package/katas/016-retry-and-schedule/solution.ts +23 -0
  51. package/katas/017-parallel-effects/SENSEI.md +70 -0
  52. package/katas/017-parallel-effects/solution.test.ts +33 -0
  53. package/katas/017-parallel-effects/solution.ts +17 -0
  54. package/katas/018-race-and-timeout/SENSEI.md +75 -0
  55. package/katas/018-race-and-timeout/solution.test.ts +30 -0
  56. package/katas/018-race-and-timeout/solution.ts +27 -0
  57. package/katas/019-ref-and-state/SENSEI.md +72 -0
  58. package/katas/019-ref-and-state/solution.test.ts +29 -0
  59. package/katas/019-ref-and-state/solution.ts +16 -0
  60. package/katas/020-fibers/SENSEI.md +80 -0
  61. package/katas/020-fibers/solution.test.ts +23 -0
  62. package/katas/020-fibers/solution.ts +23 -0
  63. package/katas/021-acquire-release/SENSEI.md +57 -0
  64. package/katas/021-acquire-release/solution.test.ts +23 -0
  65. package/katas/021-acquire-release/solution.ts +22 -0
  66. package/katas/022-scoped-layers/SENSEI.md +52 -0
  67. package/katas/022-scoped-layers/solution.test.ts +35 -0
  68. package/katas/022-scoped-layers/solution.ts +19 -0
  69. package/katas/023-resource-patterns/SENSEI.md +52 -0
  70. package/katas/023-resource-patterns/solution.test.ts +20 -0
  71. package/katas/023-resource-patterns/solution.ts +13 -0
  72. package/katas/024-streams-basics/SENSEI.md +61 -0
  73. package/katas/024-streams-basics/solution.test.ts +30 -0
  74. package/katas/024-streams-basics/solution.ts +16 -0
  75. package/katas/025-stream-operations/SENSEI.md +59 -0
  76. package/katas/025-stream-operations/solution.test.ts +26 -0
  77. package/katas/025-stream-operations/solution.ts +17 -0
  78. package/katas/026-combining-streams/SENSEI.md +54 -0
  79. package/katas/026-combining-streams/solution.test.ts +20 -0
  80. package/katas/026-combining-streams/solution.ts +16 -0
  81. package/katas/027-data-pipelines/SENSEI.md +58 -0
  82. package/katas/027-data-pipelines/solution.test.ts +22 -0
  83. package/katas/027-data-pipelines/solution.ts +16 -0
  84. package/katas/028-logging-and-spans/SENSEI.md +58 -0
  85. package/katas/028-logging-and-spans/solution.test.ts +50 -0
  86. package/katas/028-logging-and-spans/solution.ts +20 -0
  87. package/katas/029-http-client/SENSEI.md +59 -0
  88. package/katas/029-http-client/solution.test.ts +49 -0
  89. package/katas/029-http-client/solution.ts +24 -0
  90. package/katas/030-capstone/SENSEI.md +63 -0
  91. package/katas/030-capstone/solution.test.ts +67 -0
  92. package/katas/030-capstone/solution.ts +55 -0
  93. package/katas/031-config-and-environment/SENSEI.md +77 -0
  94. package/katas/031-config-and-environment/solution.test.ts +38 -0
  95. package/katas/031-config-and-environment/solution.ts +11 -0
  96. package/katas/032-cause-and-defects/SENSEI.md +90 -0
  97. package/katas/032-cause-and-defects/solution.test.ts +50 -0
  98. package/katas/032-cause-and-defects/solution.ts +23 -0
  99. package/katas/033-pattern-matching/SENSEI.md +86 -0
  100. package/katas/033-pattern-matching/solution.test.ts +36 -0
  101. package/katas/033-pattern-matching/solution.ts +28 -0
  102. package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
  103. package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
  104. package/katas/034-deferred-and-coordination/solution.ts +24 -0
  105. package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
  106. package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
  107. package/katas/035-queue-and-backpressure/solution.ts +21 -0
  108. package/katas/036-schema-advanced/SENSEI.md +81 -0
  109. package/katas/036-schema-advanced/solution.test.ts +55 -0
  110. package/katas/036-schema-advanced/solution.ts +19 -0
  111. package/katas/037-cache-and-memoization/SENSEI.md +73 -0
  112. package/katas/037-cache-and-memoization/solution.test.ts +47 -0
  113. package/katas/037-cache-and-memoization/solution.ts +24 -0
  114. package/katas/038-metrics/SENSEI.md +91 -0
  115. package/katas/038-metrics/solution.test.ts +39 -0
  116. package/katas/038-metrics/solution.ts +23 -0
  117. package/katas/039-managed-runtime/SENSEI.md +75 -0
  118. package/katas/039-managed-runtime/solution.test.ts +29 -0
  119. package/katas/039-managed-runtime/solution.ts +19 -0
  120. package/katas/040-request-batching/SENSEI.md +87 -0
  121. package/katas/040-request-batching/solution.test.ts +56 -0
  122. package/katas/040-request-batching/solution.ts +32 -0
  123. package/package.json +22 -0
  124. package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
  125. package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
  126. package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
  127. package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
  128. package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
  129. package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
  130. package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
  131. package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
  132. package/skills/effect-patterns-error-management/SKILL.md +1668 -0
  133. package/skills/effect-patterns-getting-started/SKILL.md +237 -0
  134. package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
  135. package/skills/effect-patterns-observability/SKILL.md +1586 -0
  136. package/skills/effect-patterns-platform/SKILL.md +1195 -0
  137. package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
  138. package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
  139. package/skills/effect-patterns-resource-management/SKILL.md +827 -0
  140. package/skills/effect-patterns-scheduling/SKILL.md +451 -0
  141. package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
  142. package/skills/effect-patterns-streams/SKILL.md +2052 -0
  143. package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
  144. package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
  145. package/skills/effect-patterns-testing/SKILL.md +1632 -0
  146. package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
  147. package/skills/effect-patterns-value-handling/SKILL.md +676 -0
  148. package/tsconfig.json +20 -0
  149. package/vitest.config.ts +3 -0
@@ -0,0 +1,3199 @@
1
+ ---
2
+ name: effect-patterns-core-concepts
3
+ description: Effect-TS patterns for Core Concepts. Use when working with core concepts in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Core Concepts
6
+ This skill provides 49 curated Effect-TS patterns for core concepts.
7
+ Use this skill when working on tasks related to:
8
+ - core concepts
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟢 Beginner Patterns
15
+
16
+ ### Combining Values with zip
17
+
18
+ **Rule:** Use zip to run two computations and combine their results into a tuple, preserving error and context handling.
19
+
20
+ **Good Example:**
21
+
22
+ ```typescript
23
+ import { Effect, Either, Option, Stream } from "effect";
24
+
25
+ // Effect: Combine two effects and get both results
26
+ const effectA = Effect.succeed(1);
27
+ const effectB = Effect.succeed("hello");
28
+ const zippedEffect = effectA.pipe(Effect.zip(effectB)); // Effect<[number, string]>
29
+
30
+ // Option: Combine two options, only Some if both are Some
31
+ const optionA = Option.some(1);
32
+ const optionB = Option.some("hello");
33
+ const zippedOption = Option.all([optionA, optionB]); // Option<[number, string]>
34
+
35
+ // Either: Combine two eithers, only Right if both are Right
36
+ const eitherA = Either.right(1);
37
+ const eitherB = Either.right("hello");
38
+ const zippedEither = Either.all([eitherA, eitherB]); // Either<never, [number, string]>
39
+
40
+ // Stream: Pair up values from two streams
41
+ const streamA = Stream.fromIterable([1, 2, 3]);
42
+ const streamB = Stream.fromIterable(["a", "b", "c"]);
43
+ const zippedStream = streamA.pipe(Stream.zip(streamB)); // Stream<[number, string]>
44
+ ```
45
+
46
+ **Explanation:**
47
+ `zip` runs both computations and pairs their results.
48
+ If either computation fails (or is None/Left/empty), the result is a failure (or None/Left/empty).
49
+
50
+ **Anti-Pattern:**
51
+
52
+ Manually running two computations, extracting their results, and pairing them outside the combinator world.
53
+ This breaks composability, loses error/context handling, and can lead to subtle bugs.
54
+
55
+ **Rationale:**
56
+
57
+ Use the `zip` combinator to combine two computations, pairing their results together.
58
+ This works for `Effect`, `Stream`, `Option`, and `Either`, and is useful when you want to run two computations and work with both results.
59
+
60
+
61
+ `zip` lets you compose computations that are independent but whose results you want to use together.
62
+ It preserves error handling and context, and keeps your code declarative and type-safe.
63
+
64
+ ---
65
+
66
+ ### Creating from Synchronous and Callback Code
67
+
68
+ **Rule:** Use sync and async to create Effects from synchronous or callback-based computations, making them composable and type-safe.
69
+
70
+ **Good Example:**
71
+
72
+ ```typescript
73
+ import { Effect } from "effect";
74
+
75
+ // Synchronous: Wrap a computation that is guaranteed not to throw
76
+ const effectSync = Effect.sync(() => Math.random()); // Effect<never, number, never>
77
+
78
+ // Callback-based: Wrap a Node.js-style callback API
79
+ function legacyReadFile(
80
+ path: string,
81
+ cb: (err: Error | null, data?: string) => void
82
+ ) {
83
+ setTimeout(() => cb(null, "file contents"), 10);
84
+ }
85
+
86
+ const effectAsync = Effect.async<string, Error>((resume) => {
87
+ legacyReadFile("file.txt", (err, data) => {
88
+ if (err) resume(Effect.fail(err));
89
+ else resume(Effect.succeed(data!));
90
+ });
91
+ }); // Effect<string, Error, never>
92
+ ```
93
+
94
+ **Explanation:**
95
+
96
+ - `Effect.sync` is for synchronous computations that are guaranteed not to throw.
97
+ - `Effect.async` is for integrating callback-based APIs, converting them into Effects.
98
+
99
+ **Anti-Pattern:**
100
+
101
+ Directly calling synchronous or callback-based APIs inside Effects without lifting them, which can break composability and error handling.
102
+
103
+ **Rationale:**
104
+
105
+ Use the `sync` and `async` constructors to lift synchronous or callback-based computations into the Effect world.
106
+ This enables safe, composable interop with legacy or third-party code that doesn't use Promises or Effects.
107
+
108
+
109
+ Many APIs are synchronous or use callbacks instead of Promises.
110
+ By lifting them into Effects, you gain access to all of Effect's combinators, error handling, and resource safety.
111
+
112
+ ---
113
+
114
+ ### Model Optional Values Safely with Option
115
+
116
+ **Rule:** Use Option to model values that may be present or absent, making absence explicit and type-safe.
117
+
118
+ **Good Example:**
119
+
120
+ ```typescript
121
+ import { Option } from "effect";
122
+
123
+ // Create an Option from a value
124
+ const someValue = Option.some(42); // Option<number>
125
+ const noValue = Option.none(); // Option<never>
126
+
127
+ // Safely convert a nullable value to Option
128
+ const fromNullable = Option.fromNullable(Math.random() > 0.5 ? "hello" : null); // Option<string>
129
+
130
+ // Pattern match on Option
131
+ const result = someValue.pipe(
132
+ Option.match({
133
+ onNone: () => "No value",
134
+ onSome: (n) => `Value: ${n}`,
135
+ })
136
+ ); // string
137
+
138
+ // Use Option in a workflow
139
+ function findUser(id: number): Option.Option<{ id: number; name: string }> {
140
+ return id === 1 ? Option.some({ id, name: "Alice" }) : Option.none();
141
+ }
142
+ ```
143
+
144
+ **Explanation:**
145
+
146
+ - `Option.some(value)` represents a present value.
147
+ - `Option.none()` represents absence.
148
+ - `Option.fromNullable` safely lifts nullable values into Option.
149
+ - Pattern matching ensures all cases are handled.
150
+
151
+ **Anti-Pattern:**
152
+
153
+ Using `null` or `undefined` to represent absence, or forgetting to handle the "no value" case, which leads to runtime errors and less maintainable code.
154
+
155
+ **Rationale:**
156
+
157
+ Use the `Option<A>` data type to represent values that may or may not exist.
158
+ This eliminates the need for `null` or `undefined`, making absence explicit and type-safe.
159
+
160
+
161
+ `Option` makes it impossible to forget to handle the "no value" case.
162
+ It improves code safety, readability, and composability, and is a foundation for robust domain modeling.
163
+
164
+ ---
165
+
166
+ ### Comparing Data by Value with Structural Equality
167
+
168
+ **Rule:** Use Data.struct or implement the Equal interface for value-based comparison of objects and classes.
169
+
170
+ **Good Example:**
171
+
172
+ We define two points using `Data.struct`. Even though `p1` and `p2` are different instances in memory, `Equal.equals` correctly reports them as equal because their contents match.
173
+
174
+ ```typescript
175
+ import { Data, Equal, Effect } from "effect";
176
+
177
+ // Define a Point type with structural equality
178
+ interface Point {
179
+ readonly _tag: "Point";
180
+ readonly x: number;
181
+ readonly y: number;
182
+ }
183
+
184
+ const Point = Data.tagged<Point>("Point");
185
+
186
+ // Create a program to demonstrate structural equality
187
+ const program = Effect.gen(function* () {
188
+ const p1 = Point({ x: 1, y: 2 });
189
+ const p2 = Point({ x: 1, y: 2 });
190
+ const p3 = Point({ x: 3, y: 4 });
191
+
192
+ // Standard reference equality fails
193
+ yield* Effect.log("Comparing points with reference equality (===):");
194
+ yield* Effect.log(`p1 === p2: ${p1 === p2}`);
195
+
196
+ // Structural equality works as expected
197
+ yield* Effect.log("\nComparing points with structural equality:");
198
+ yield* Effect.log(`p1 equals p2: ${Equal.equals(p1, p2)}`);
199
+ yield* Effect.log(`p1 equals p3: ${Equal.equals(p1, p3)}`);
200
+
201
+ // Show the actual points
202
+ yield* Effect.log("\nPoint values:");
203
+ yield* Effect.log(`p1: ${JSON.stringify(p1)}`);
204
+ yield* Effect.log(`p2: ${JSON.stringify(p2)}`);
205
+ yield* Effect.log(`p3: ${JSON.stringify(p3)}`);
206
+ });
207
+
208
+ // Run the program
209
+ Effect.runPromise(program);
210
+ ```
211
+
212
+ ---
213
+
214
+ **Anti-Pattern:**
215
+
216
+ Relying on `===` for object or array comparison. This will lead to bugs when you expect two objects with the same values to be treated as equal, especially when working with data in collections, `Ref`s, or `Effect`'s success values.
217
+
218
+ ```typescript
219
+ // ❌ WRONG: This will not behave as expected.
220
+ const user1 = { id: 1, name: "Paul" };
221
+ const user2 = { id: 1, name: "Paul" };
222
+
223
+ if (user1 === user2) {
224
+ // This code block will never be reached.
225
+ console.log("Users are the same.");
226
+ }
227
+
228
+ // Another common pitfall
229
+ const selectedUsers = [user1];
230
+ // This check will fail, even though a user with id 1 is in the array.
231
+ if (selectedUsers.includes({ id: 1, name: "Paul" })) {
232
+ // ...
233
+ }
234
+ ```
235
+
236
+ **Rationale:**
237
+
238
+ To compare objects or classes by their contents rather than by their memory reference, use one of two methods:
239
+
240
+ 1. **For plain data objects:** Define them with `Data.struct`.
241
+ 2. **For classes:** Extend `Data.Class` or implement the `Equal.Equal` interface.
242
+
243
+ Then, compare instances using the `Equal.equals(a, b)` function.
244
+
245
+ ---
246
+
247
+
248
+ In JavaScript, comparing two non-primitive values with `===` checks for _referential equality_. It only returns `true` if they are the exact same instance in memory. This means two objects with identical contents are not considered equal, which is a common source of bugs.
249
+
250
+ ```typescript
251
+ { a: 1 } === { a: 1 } // false!
252
+ ```
253
+
254
+ Effect solves this with **structural equality**. All of Effect's built-in data structures (`Option`, `Either`, `Chunk`, etc.) can be compared by their structure and values. By using helpers like `Data.struct`, you can easily give your own data structures this same powerful and predictable behavior.
255
+
256
+ ---
257
+
258
+ ---
259
+
260
+ ### Accumulate Multiple Errors with Either
261
+
262
+ **Rule:** Use Either to model computations that may fail, making errors explicit and type-safe.
263
+
264
+ **Good Example:**
265
+
266
+ ```typescript
267
+ import { Either } from "effect";
268
+
269
+ // Create a Right (success) or Left (failure)
270
+ const success = Either.right(42); // Either<never, number>
271
+ const failure = Either.left("Something went wrong"); // Either<string, never>
272
+
273
+ // Pattern match on Either
274
+ const result = success.pipe(
275
+ Either.match({
276
+ onLeft: (err) => `Error: ${err}`,
277
+ onRight: (value) => `Value: ${value}`,
278
+ })
279
+ ); // string
280
+
281
+ // Combine multiple Eithers and accumulate errors
282
+ const e1 = Either.right(1);
283
+ const e2 = Either.left("fail1");
284
+ const e3 = Either.left("fail2");
285
+
286
+ const all = [e1, e2, e3].filter(Either.isRight).map(Either.getRight); // [1]
287
+ const errors = [e1, e2, e3].filter(Either.isLeft).map(Either.getLeft); // ["fail1", "fail2"]
288
+ ```
289
+
290
+ **Explanation:**
291
+
292
+ - `Either.right(value)` represents success.
293
+ - `Either.left(error)` represents failure.
294
+ - Pattern matching ensures all cases are handled.
295
+ - You can accumulate errors or results from multiple Eithers.
296
+
297
+ **Anti-Pattern:**
298
+
299
+ Throwing exceptions or using ad-hoc error codes, which are not type-safe, not composable, and make error handling less predictable.
300
+
301
+ **Rationale:**
302
+
303
+ Use the `Either<E, A>` data type to represent computations that can fail (`Left<E>`) or succeed (`Right<A>`).
304
+ This makes error handling explicit, type-safe, and composable.
305
+
306
+
307
+ `Either` is a foundational data type for error handling in functional programming.
308
+ It allows you to accumulate errors, model domain-specific failures, and avoid exceptions and unchecked errors.
309
+
310
+ ---
311
+
312
+ ### Lifting Values with succeed, some, and right
313
+
314
+ **Rule:** Use succeed, some, and right to create Effect, Option, or Either from plain values.
315
+
316
+ **Good Example:**
317
+
318
+ ```typescript
319
+ import { Effect, Option, Either } from "effect";
320
+
321
+ // Effect: Lift a value into an Effect that always succeeds
322
+ const effect = Effect.succeed(42); // Effect<never, number, never>
323
+
324
+ // Option: Lift a value into an Option that is always Some
325
+ const option = Option.some("hello"); // Option<string>
326
+
327
+ // Either: Lift a value into an Either that is always Right
328
+ const either = Either.right({ id: 1 }); // Either<never, { id: number }>
329
+ ```
330
+
331
+ **Explanation:**
332
+
333
+ - `Effect.succeed(value)` creates an effect that always succeeds with `value`.
334
+ - `Option.some(value)` creates an option that is always present.
335
+ - `Either.right(value)` creates an either that always represents success.
336
+
337
+ **Anti-Pattern:**
338
+
339
+ Passing plain values around outside the Effect, Option, or Either world, or using `null`/`undefined` to represent absence or success.
340
+ This leads to less composable, less type-safe code and makes error handling harder.
341
+
342
+ **Rationale:**
343
+
344
+ Use the `succeed`, `some`, and `right` constructors to lift plain values into the Effect, Option, or Either world.
345
+ This is the foundation for building composable, type-safe programs.
346
+
347
+
348
+ Lifting values into these structures allows you to compose them with other effects, options, or eithers, and to take advantage of all the combinators and error handling that Effect provides.
349
+
350
+ ---
351
+
352
+ ### Understand that Effects are Lazy Blueprints
353
+
354
+ **Rule:** Understand that effects are lazy blueprints.
355
+
356
+ **Good Example:**
357
+
358
+ ```typescript
359
+ import { Effect } from "effect";
360
+
361
+ Effect.runSync(Effect.log("1. Defining the Effect blueprint..."));
362
+
363
+ const program = Effect.gen(function* () {
364
+ yield* Effect.log("3. The blueprint is now being executed!");
365
+ return 42;
366
+ });
367
+
368
+ const demonstrationProgram = Effect.gen(function* () {
369
+ yield* Effect.log(
370
+ "2. The blueprint has been defined. No work has been done yet."
371
+ );
372
+ yield* program;
373
+ });
374
+
375
+ Effect.runSync(demonstrationProgram);
376
+ ```
377
+
378
+ **Explanation:**
379
+ Defining an `Effect` does not execute any code inside it. Only when you call
380
+ `Effect.runSync(program)` does the computation actually happen.
381
+
382
+ **Anti-Pattern:**
383
+
384
+ Assuming an `Effect` behaves like a `Promise`. A `Promise` executes its work
385
+ immediately upon creation. Never expect a side effect to occur just from
386
+ defining an `Effect`.
387
+
388
+ **Rationale:**
389
+
390
+ An `Effect` is not a value or a `Promise`. It is a lazy, immutable blueprint
391
+ that describes a computation. It does nothing on its own until it is passed to
392
+ a runtime executor (e.g., `Effect.runPromise` or `Effect.runSync`).
393
+
394
+
395
+ This laziness is a superpower because it makes your code composable,
396
+ predictable, and testable. Unlike a `Promise` which executes immediately,
397
+ an `Effect` is just a description of work, like a recipe waiting for a chef.
398
+
399
+ ---
400
+
401
+ ### Conditional Branching with if, when, and cond
402
+
403
+ **Rule:** Use combinators such as if, when, and cond to branch computations based on runtime conditions, without imperative if statements.
404
+
405
+ **Good Example:**
406
+
407
+ ```typescript
408
+ import { Effect, Stream, Option, Either } from "effect";
409
+
410
+ // Effect: Branch based on a condition
411
+ const effect = Effect.if(true, {
412
+ onTrue: () => Effect.succeed("yes"),
413
+ onFalse: () => Effect.succeed("no"),
414
+ }); // Effect<string>
415
+
416
+ // Option: Conditionally create an Option
417
+ const option = true ? Option.some("yes") : Option.none(); // Option<string> (Some("yes"))
418
+
419
+ // Either: Conditionally create an Either
420
+ const either = true ? Either.right("yes") : Either.left("error"); // Either<string, string> (Right("yes"))
421
+
422
+ // Stream: Conditionally emit a stream
423
+ const stream = false ? Stream.fromIterable([1, 2]) : Stream.empty; // Stream<number> (empty)
424
+ ```
425
+
426
+ **Explanation:**
427
+ These combinators let you branch your computation based on a boolean or predicate, without leaving the world of composable, type-safe code.
428
+ You can also use `when` to run an effect only if a condition is true, or `unless` to run it only if a condition is false.
429
+
430
+ **Anti-Pattern:**
431
+
432
+ Using imperative `if` statements to decide which effect, option, either, or stream to return, breaking composability and making error/context handling less predictable.
433
+
434
+ **Rationale:**
435
+
436
+ Use combinators like `if`, `when`, and `cond` to express conditional logic in a declarative, composable way.
437
+ These combinators allow you to branch computations based on runtime conditions, without resorting to imperative `if` statements.
438
+
439
+
440
+ Declarative branching keeps your code composable, testable, and easy to reason about.
441
+ It also ensures that error handling and context propagation are preserved, and that your code remains consistent across different Effect types.
442
+
443
+ ---
444
+
445
+ ### Transforming Values with map
446
+
447
+ **Rule:** Use map to apply a pure function to the value inside an Effect, Stream, Option, or Either.
448
+
449
+ **Good Example:**
450
+
451
+ ```typescript
452
+ import { Effect, Stream, Option, Either } from "effect";
453
+
454
+ // Effect: Transform the result of an effect
455
+ const effect = Effect.succeed(2).pipe(Effect.map((n) => n * 10)); // Effect<number>
456
+
457
+ // Option: Transform an optional value
458
+ const option = Option.some(2).pipe(Option.map((n) => n * 10)); // Option<number>
459
+
460
+ // Either: Transform a value that may be an error
461
+ const either = Either.right(2).pipe(Either.map((n) => n * 10)); // Either<never, number>
462
+
463
+ // Stream: Transform every value in a stream
464
+ const stream = Stream.fromIterable([1, 2, 3]).pipe(Stream.map((n) => n * 10)); // Stream<number>
465
+ ```
466
+
467
+ **Explanation:**
468
+ No matter which type you use, `map` lets you apply a function to the value inside, without changing the error or context.
469
+
470
+ **Anti-Pattern:**
471
+
472
+ Manually extracting the value (e.g., with `.getOrElse`, `.unsafeRunSync`, or similar) just to transform it, then re-wrapping it.
473
+ This breaks composability and loses the benefits of type safety and error handling.
474
+
475
+ **Rationale:**
476
+
477
+ Use the `map` combinator to apply a pure function to the value inside an `Effect`, `Stream`, `Option`, or `Either`.
478
+ This lets you transform results without changing the structure or error-handling behavior of the original type.
479
+
480
+
481
+ `map` is the most fundamental combinator in functional programming.
482
+ It allows you to focus on _what_ you want to do with a value, not _how_ to extract it.
483
+ The same mental model applies across all major Effect types.
484
+
485
+ ---
486
+
487
+ ### Chaining Computations with flatMap
488
+
489
+ **Rule:** Use flatMap to sequence computations, flattening nested structures and preserving error and context handling.
490
+
491
+ **Good Example:**
492
+
493
+ ```typescript
494
+ import { Effect, Stream, Option, Either } from "effect";
495
+
496
+ // Effect: Chain two effectful computations
497
+ const effect = Effect.succeed(2).pipe(
498
+ Effect.flatMap((n) => Effect.succeed(n * 10))
499
+ ); // Effect<number>
500
+
501
+ // Option: Chain two optional computations
502
+ const option = Option.some(2).pipe(Option.flatMap((n) => Option.some(n * 10))); // Option<number>
503
+
504
+ // Either: Chain two computations that may fail
505
+ const either = Either.right(2).pipe(
506
+ Either.flatMap((n) => Either.right(n * 10))
507
+ ); // Either<never, number>
508
+
509
+ // Stream: Chain streams (flattening)
510
+ const stream = Stream.fromIterable([1, 2]).pipe(
511
+ Stream.flatMap((n) => Stream.fromIterable([n, n * 10]))
512
+ ); // Stream<number>
513
+ ```
514
+
515
+ **Explanation:**
516
+ `flatMap` lets you build pipelines where each step can depend on the result of the previous one, and the structure is always flattened—no `Option<Option<A>>` or `Effect<Effect<A>>`.
517
+
518
+ **Anti-Pattern:**
519
+
520
+ Manually unwrapping the value (e.g., with `.getOrElse`, `.unsafeRunSync`, etc.), then creating a new effect/option/either/stream.
521
+ This breaks composability, loses error/context handling, and leads to deeply nested or unsafe code.
522
+
523
+ **Rationale:**
524
+
525
+ Use the `flatMap` combinator to chain together computations where each step may itself return an `Effect`, `Stream`, `Option`, or `Either`.
526
+ `flatMap` ensures that the result is always "flattened"—you never get nested types.
527
+
528
+
529
+ `flatMap` is the key to sequencing dependent steps in functional programming.
530
+ It allows you to express workflows where each step may fail, be optional, or produce multiple results, and ensures that errors and context are handled automatically.
531
+
532
+ ---
533
+
534
+ ### Filtering Results with filter
535
+
536
+ **Rule:** Use filter to declaratively express conditional logic, keeping only values that satisfy a predicate.
537
+
538
+ **Good Example:**
539
+
540
+ ```typescript
541
+ import { Effect, Stream, Option, Either } from "effect";
542
+
543
+ // Effect: Only succeed if the value is even, fail otherwise
544
+ const effect = Effect.succeed(4).pipe(
545
+ Effect.filterOrFail(
546
+ (n): n is number => n % 2 === 0,
547
+ () => "Number is not even"
548
+ )
549
+ ); // Effect<number, string>
550
+
551
+ // Option: Only keep the value if it is even
552
+ const option = Option.some(4).pipe(
553
+ Option.filter((n): n is number => n % 2 === 0)
554
+ ); // Option<number>
555
+
556
+ // Either: Use map and flatMap to filter
557
+ const either = Either.right(4).pipe(
558
+ Either.flatMap((n) =>
559
+ n % 2 === 0 ? Either.right(n) : Either.left("Number is not even")
560
+ )
561
+ ); // Either<string, number>
562
+
563
+ // Stream: Only emit even numbers
564
+ const stream = Stream.fromIterable([1, 2, 3, 4]).pipe(
565
+ Stream.filter((n): n is number => n % 2 === 0)
566
+ ); // Stream<number>
567
+ ```
568
+
569
+ **Explanation:**
570
+ `filter` applies a predicate to the value(s) inside the structure. If the predicate fails, the result is a failure (`Effect.fail`, `Either.left`), `Option.none`, or an empty stream.
571
+
572
+ **Anti-Pattern:**
573
+
574
+ Using `map` with a conditional that returns `Option` or `Either`, then manually flattening, instead of using `filter`.
575
+ This leads to unnecessary complexity and less readable code.
576
+
577
+ **Rationale:**
578
+
579
+ Use the `filter` combinator to keep only those values that satisfy a predicate.
580
+ This works for `Effect`, `Stream`, `Option`, and `Either`, allowing you to express conditional logic declaratively and safely.
581
+
582
+
583
+ `filter` lets you express "only continue if..." logic without resorting to manual checks or imperative branching.
584
+ It keeps your code composable and type-safe, and ensures that failures or empty results are handled consistently.
585
+
586
+ ---
587
+
588
+ ### Comparing Data by Value with Data.struct
589
+
590
+ **Rule:** Use Data.struct to define objects whose equality is based on their contents, enabling safe and predictable comparisons.
591
+
592
+ **Good Example:**
593
+
594
+ ```typescript
595
+ import { Data, Equal } from "effect";
596
+
597
+ // Create two structurally equal objects
598
+ const user1 = Data.struct({ id: 1, name: "Alice" });
599
+ const user2 = Data.struct({ id: 1, name: "Alice" });
600
+
601
+ // Compare by value, not reference
602
+ const areEqual = Equal.equals(user1, user2); // true
603
+
604
+ // Use in a HashSet or as keys in a Map
605
+ import { HashSet } from "effect";
606
+ const set = HashSet.make(user1);
607
+ console.log(HashSet.has(set, user2)); // true
608
+ ```
609
+
610
+ **Explanation:**
611
+
612
+ - `Data.struct` creates immutable objects with value-based equality.
613
+ - Use for domain entities, value objects, and when storing objects in sets or as map keys.
614
+ - Avoids bugs from reference-based comparison.
615
+
616
+ **Anti-Pattern:**
617
+
618
+ Using plain JavaScript objects for value-based logic, which compares by reference and can lead to incorrect equality checks and collection behavior.
619
+
620
+ **Rationale:**
621
+
622
+ Use `Data.struct` to create immutable, structurally-typed objects whose equality is based on their contents, not their reference.
623
+ This enables safe, predictable comparisons and is ideal for domain modeling.
624
+
625
+
626
+ JavaScript objects are compared by reference, which can lead to subtle bugs when modeling value objects.
627
+ `Data.struct` ensures that two objects with the same contents are considered equal, supporting value-based logic and collections.
628
+
629
+ ---
630
+
631
+ ### Wrap Asynchronous Computations with tryPromise
632
+
633
+ **Rule:** Wrap asynchronous computations with tryPromise.
634
+
635
+ **Good Example:**
636
+
637
+ ```typescript
638
+ import { Effect, Data } from "effect";
639
+
640
+ // Define error type using Data.TaggedError
641
+ class HttpError extends Data.TaggedError("HttpError")<{
642
+ readonly message: string;
643
+ }> {}
644
+
645
+ // Define HTTP client service
646
+ export class HttpClient extends Effect.Service<HttpClient>()("HttpClient", {
647
+ // Provide default implementation
648
+ sync: () => ({
649
+ getUrl: (url: string) =>
650
+ Effect.tryPromise({
651
+ try: () => fetch(url),
652
+ catch: (error) =>
653
+ new HttpError({ message: `Failed to fetch ${url}: ${error}` }),
654
+ }),
655
+ }),
656
+ }) {}
657
+
658
+ // Mock HTTP client for demonstration
659
+ export class MockHttpClient extends Effect.Service<MockHttpClient>()(
660
+ "MockHttpClient",
661
+ {
662
+ sync: () => ({
663
+ getUrl: (url: string) =>
664
+ Effect.gen(function* () {
665
+ yield* Effect.logInfo(`Fetching URL: ${url}`);
666
+
667
+ // Simulate different responses based on URL
668
+ if (url.includes("success")) {
669
+ yield* Effect.logInfo("✅ Request successful");
670
+ return new Response(JSON.stringify({ data: "success" }), {
671
+ status: 200,
672
+ });
673
+ } else if (url.includes("error")) {
674
+ yield* Effect.logInfo("❌ Request failed");
675
+ return yield* Effect.fail(
676
+ new HttpError({ message: "Server returned 500" })
677
+ );
678
+ } else {
679
+ yield* Effect.logInfo("✅ Request completed");
680
+ return new Response(JSON.stringify({ data: "mock response" }), {
681
+ status: 200,
682
+ });
683
+ }
684
+ }),
685
+ }),
686
+ }
687
+ ) {}
688
+
689
+ // Demonstrate wrapping asynchronous computations
690
+ const program = Effect.gen(function* () {
691
+ yield* Effect.logInfo("=== Wrapping Asynchronous Computations Demo ===");
692
+
693
+ const client = yield* MockHttpClient;
694
+
695
+ // Example 1: Successful request
696
+ yield* Effect.logInfo("\n1. Successful request:");
697
+ const response1 = yield* client
698
+ .getUrl("https://api.example.com/success")
699
+ .pipe(
700
+ Effect.catchAll((error) =>
701
+ Effect.gen(function* () {
702
+ yield* Effect.logError(`Request failed: ${error.message}`);
703
+ return new Response("Error response", { status: 500 });
704
+ })
705
+ )
706
+ );
707
+ yield* Effect.logInfo(`Response status: ${response1.status}`);
708
+
709
+ // Example 2: Failed request with error handling
710
+ yield* Effect.logInfo("\n2. Failed request with error handling:");
711
+ const response2 = yield* client.getUrl("https://api.example.com/error").pipe(
712
+ Effect.catchAll((error) =>
713
+ Effect.gen(function* () {
714
+ yield* Effect.logError(`Request failed: ${error.message}`);
715
+ return new Response("Fallback response", { status: 200 });
716
+ })
717
+ )
718
+ );
719
+ yield* Effect.logInfo(`Fallback response status: ${response2.status}`);
720
+
721
+ // Example 3: Multiple async operations
722
+ yield* Effect.logInfo("\n3. Multiple async operations:");
723
+ const results = yield* Effect.all(
724
+ [
725
+ client.getUrl("https://api.example.com/endpoint1"),
726
+ client.getUrl("https://api.example.com/endpoint2"),
727
+ client.getUrl("https://api.example.com/endpoint3"),
728
+ ],
729
+ { concurrency: 2 }
730
+ ).pipe(
731
+ Effect.catchAll((error) =>
732
+ Effect.gen(function* () {
733
+ yield* Effect.logError(`One or more requests failed: ${error.message}`);
734
+ return [];
735
+ })
736
+ )
737
+ );
738
+ yield* Effect.logInfo(`Completed ${results.length} requests`);
739
+
740
+ yield* Effect.logInfo(
741
+ "\n✅ Asynchronous computations demonstration completed!"
742
+ );
743
+ });
744
+
745
+ // Run with mock implementation
746
+ Effect.runPromise(Effect.provide(program, MockHttpClient.Default));
747
+ ```
748
+
749
+ **Explanation:**
750
+ `Effect.tryPromise` wraps a `Promise`-returning function and safely handles
751
+ rejections, moving errors into the Effect's error channel.
752
+
753
+ **Anti-Pattern:**
754
+
755
+ Manually handling `.then()` and `.catch()` inside an `Effect.sync`. This is
756
+ verbose, error-prone, and defeats the purpose of using Effect's built-in
757
+ Promise integration.
758
+
759
+ **Rationale:**
760
+
761
+ To integrate a `Promise`-based function (like `fetch`), use `Effect.tryPromise`.
762
+
763
+
764
+ This is the standard bridge from the Promise-based world to Effect, allowing
765
+ you to leverage the massive `async/await` ecosystem safely.
766
+
767
+ ---
768
+
769
+ ### Write Sequential Code with Effect.gen
770
+
771
+ **Rule:** Write sequential code with Effect.gen.
772
+
773
+ **Good Example:**
774
+
775
+ ```typescript
776
+ import { Effect } from "effect";
777
+
778
+ // Mock API functions for demonstration
779
+ const fetchUser = (id: number) =>
780
+ Effect.gen(function* () {
781
+ yield* Effect.logInfo(`Fetching user ${id}...`);
782
+ // Simulate API call
783
+ yield* Effect.sleep("100 millis");
784
+ return { id, name: `User ${id}`, email: `user${id}@example.com` };
785
+ });
786
+
787
+ const fetchUserPosts = (userId: number) =>
788
+ Effect.gen(function* () {
789
+ yield* Effect.logInfo(`Fetching posts for user ${userId}...`);
790
+ // Simulate API call
791
+ yield* Effect.sleep("150 millis");
792
+ return [
793
+ { id: 1, title: "First Post", userId },
794
+ { id: 2, title: "Second Post", userId },
795
+ ];
796
+ });
797
+
798
+ const fetchPostComments = (postId: number) =>
799
+ Effect.gen(function* () {
800
+ yield* Effect.logInfo(`Fetching comments for post ${postId}...`);
801
+ // Simulate API call
802
+ yield* Effect.sleep("75 millis");
803
+ return [
804
+ { id: 1, text: "Great post!", postId },
805
+ { id: 2, text: "Thanks for sharing", postId },
806
+ ];
807
+ });
808
+
809
+ // Example of sequential code with Effect.gen
810
+ const getUserDataWithGen = (userId: number) =>
811
+ Effect.gen(function* () {
812
+ // Step 1: Fetch user
813
+ const user = yield* fetchUser(userId);
814
+ yield* Effect.logInfo(`✅ Got user: ${user.name}`);
815
+
816
+ // Step 2: Fetch user's posts (depends on user data)
817
+ const posts = yield* fetchUserPosts(user.id);
818
+ yield* Effect.logInfo(`✅ Got ${posts.length} posts`);
819
+
820
+ // Step 3: Fetch comments for first post (depends on posts data)
821
+ const firstPost = posts[0];
822
+ const comments = yield* fetchPostComments(firstPost.id);
823
+ yield* Effect.logInfo(
824
+ `✅ Got ${comments.length} comments for "${firstPost.title}"`
825
+ );
826
+
827
+ // Step 4: Combine all data
828
+ const result = {
829
+ user,
830
+ posts,
831
+ featuredPost: {
832
+ ...firstPost,
833
+ comments,
834
+ },
835
+ };
836
+
837
+ yield* Effect.logInfo("✅ Successfully combined all user data");
838
+ return result;
839
+ });
840
+
841
+ // Example without Effect.gen (more complex)
842
+ const getUserDataWithoutGen = (userId: number) =>
843
+ fetchUser(userId).pipe(
844
+ Effect.flatMap((user) =>
845
+ fetchUserPosts(user.id).pipe(
846
+ Effect.flatMap((posts) =>
847
+ fetchPostComments(posts[0].id).pipe(
848
+ Effect.map((comments) => ({
849
+ user,
850
+ posts,
851
+ featuredPost: {
852
+ ...posts[0],
853
+ comments,
854
+ },
855
+ }))
856
+ )
857
+ )
858
+ )
859
+ )
860
+ );
861
+
862
+ // Demonstrate writing sequential code with gen
863
+ const program = Effect.gen(function* () {
864
+ yield* Effect.logInfo("=== Writing Sequential Code with Effect.gen Demo ===");
865
+
866
+ // Example 1: Sequential operations with Effect.gen
867
+ yield* Effect.logInfo("\n1. Sequential operations with Effect.gen:");
868
+ const userData = yield* getUserDataWithGen(123).pipe(
869
+ Effect.catchAll((error) =>
870
+ Effect.gen(function* () {
871
+ yield* Effect.logError(`Failed to get user data: ${error}`);
872
+ return null;
873
+ })
874
+ )
875
+ );
876
+
877
+ if (userData) {
878
+ yield* Effect.logInfo(
879
+ `Final result: User "${userData.user.name}" has ${userData.posts.length} posts`
880
+ );
881
+ yield* Effect.logInfo(
882
+ `Featured post: "${userData.featuredPost.title}" with ${userData.featuredPost.comments.length} comments`
883
+ );
884
+ }
885
+
886
+ // Example 2: Compare with traditional promise-like chaining
887
+ yield* Effect.logInfo("\n2. Same logic without Effect.gen (for comparison):");
888
+ const userData2 = yield* getUserDataWithoutGen(456).pipe(
889
+ Effect.catchAll((error) =>
890
+ Effect.gen(function* () {
891
+ yield* Effect.logError(`Failed to get user data: ${error}`);
892
+ return null;
893
+ })
894
+ )
895
+ );
896
+
897
+ if (userData2) {
898
+ yield* Effect.logInfo(
899
+ `Result from traditional approach: User "${userData2.user.name}"`
900
+ );
901
+ }
902
+
903
+ // Example 3: Error handling in sequential code
904
+ yield* Effect.logInfo("\n3. Error handling in sequential operations:");
905
+ const errorHandling = yield* Effect.gen(function* () {
906
+ try {
907
+ const user = yield* fetchUser(999);
908
+ const posts = yield* fetchUserPosts(user.id);
909
+ return { user, posts };
910
+ } catch (error) {
911
+ yield* Effect.logError(`Error in sequential operations: ${error}`);
912
+ return null;
913
+ }
914
+ }).pipe(
915
+ Effect.catchAll((error) =>
916
+ Effect.gen(function* () {
917
+ yield* Effect.logError(`Caught error: ${error}`);
918
+ return { user: null, posts: [] };
919
+ })
920
+ )
921
+ );
922
+
923
+ yield* Effect.logInfo(
924
+ `Error handling result: ${errorHandling ? "Success" : "Handled error"}`
925
+ );
926
+
927
+ yield* Effect.logInfo("\n✅ Sequential code demonstration completed!");
928
+ yield* Effect.logInfo(
929
+ "Effect.gen makes sequential async code look like synchronous code!"
930
+ );
931
+ });
932
+
933
+ Effect.runPromise(program);
934
+ ```
935
+
936
+ **Explanation:**
937
+ `Effect.gen` allows you to write top-to-bottom code that is easy to read and
938
+ maintain, even when chaining many asynchronous steps.
939
+
940
+ **Anti-Pattern:**
941
+
942
+ Deeply nesting `flatMap` calls. This is much harder to read and maintain than
943
+ the equivalent `Effect.gen` block.
944
+
945
+ **Rationale:**
946
+
947
+ For sequential operations that depend on each other, use `Effect.gen` to write
948
+ your logic in a familiar, imperative style. It's the Effect-native equivalent
949
+ of `async/await`.
950
+
951
+
952
+ `Effect.gen` uses generator functions to create a flat, linear, and highly
953
+ readable sequence of operations, avoiding the nested "callback hell" of
954
+ `flatMap`.
955
+
956
+ ---
957
+
958
+ ### Transform Effect Values with map and flatMap
959
+
960
+ **Rule:** Transform Effect values with map and flatMap.
961
+
962
+ **Good Example:**
963
+
964
+ ```typescript
965
+ import { Effect } from "effect";
966
+
967
+ const getUser = (id: number): Effect.Effect<{ id: number; name: string }> =>
968
+ Effect.succeed({ id, name: "Paul" });
969
+
970
+ const getPosts = (userId: number): Effect.Effect<{ title: string }[]> =>
971
+ Effect.succeed([{ title: "My First Post" }, { title: "Second Post" }]);
972
+
973
+ const userPosts = getUser(123).pipe(
974
+ Effect.flatMap((user) => getPosts(user.id))
975
+ );
976
+
977
+ // Demonstrate transforming Effect values
978
+ const program = Effect.gen(function* () {
979
+ yield* Effect.log("=== Transform Effect Values Demo ===");
980
+
981
+ // 1. Basic transformation with map
982
+ yield* Effect.log("\n1. Transform with map:");
983
+ const userWithUpperName = yield* getUser(123).pipe(
984
+ Effect.map((user) => ({ ...user, name: user.name.toUpperCase() }))
985
+ );
986
+ yield* Effect.log("Transformed user:", userWithUpperName);
987
+
988
+ // 2. Chain effects with flatMap
989
+ yield* Effect.log("\n2. Chain effects with flatMap:");
990
+ const posts = yield* userPosts;
991
+ yield* Effect.log("User posts:", posts);
992
+
993
+ // 3. Transform and combine multiple effects
994
+ yield* Effect.log("\n3. Transform and combine multiple effects:");
995
+ const userWithPosts = yield* getUser(456).pipe(
996
+ Effect.flatMap((user) =>
997
+ getPosts(user.id).pipe(
998
+ Effect.map((posts) => ({
999
+ user: user.name,
1000
+ postCount: posts.length,
1001
+ titles: posts.map((p) => p.title),
1002
+ }))
1003
+ )
1004
+ )
1005
+ );
1006
+ yield* Effect.log("User with posts:", userWithPosts);
1007
+
1008
+ // 4. Transform with tap for side effects
1009
+ yield* Effect.log("\n4. Transform with tap for side effects:");
1010
+ const result = yield* getUser(789).pipe(
1011
+ Effect.tap((user) => Effect.log(`Processing user: ${user.name}`)),
1012
+ Effect.map((user) => `Hello, ${user.name}!​`)
1013
+ );
1014
+ yield* Effect.log("Final result:", result);
1015
+
1016
+ yield* Effect.log("\n✅ All transformations completed successfully!");
1017
+ });
1018
+
1019
+ Effect.runPromise(program);
1020
+ ```
1021
+
1022
+ **Explanation:**
1023
+ Use `flatMap` to chain effects that depend on each other, and `map` for
1024
+ simple value transformations.
1025
+
1026
+ **Anti-Pattern:**
1027
+
1028
+ Using `map` when you should be using `flatMap`. This results in a nested
1029
+ `Effect<Effect<...>>`, which is usually not what you want.
1030
+
1031
+ **Rationale:**
1032
+
1033
+ To work with the success value of an `Effect`, use `Effect.map` for simple,
1034
+ synchronous transformations and `Effect.flatMap` for effectful transformations.
1035
+
1036
+
1037
+ `Effect.map` is like `Array.prototype.map`. `Effect.flatMap` is like
1038
+ `Promise.prototype.then` and is used when your transformation function itself
1039
+ returns an `Effect`.
1040
+
1041
+ ---
1042
+
1043
+ ### Converting from Nullable, Option, or Either
1044
+
1045
+ **Rule:** Use fromNullable, fromOption, and fromEither to lift nullable values, Option, or Either into Effects or Streams for safe, typeful interop.
1046
+
1047
+ **Good Example:**
1048
+
1049
+ ```typescript
1050
+ import { Effect, Option, Either } from "effect";
1051
+
1052
+ // Option: Convert a nullable value to an Option
1053
+ const nullableValue: string | null = Math.random() > 0.5 ? "hello" : null;
1054
+ const option = Option.fromNullable(nullableValue); // Option<string>
1055
+
1056
+ // Effect: Convert an Option to an Effect that may fail
1057
+ const someValue = Option.some(42);
1058
+ const effectFromOption = Option.match(someValue, {
1059
+ onNone: () => Effect.fail("No value"),
1060
+ onSome: (value) => Effect.succeed(value),
1061
+ }); // Effect<number, string, never>
1062
+
1063
+ // Effect: Convert an Either to an Effect
1064
+ const either = Either.right("success");
1065
+ const effectFromEither = Either.match(either, {
1066
+ onLeft: (error) => Effect.fail(error),
1067
+ onRight: (value) => Effect.succeed(value),
1068
+ }); // Effect<string, never, never>
1069
+ ```
1070
+
1071
+ **Explanation:**
1072
+
1073
+ - `Effect.fromNullable` lifts a nullable value into an Effect, failing if the value is `null` or `undefined`.
1074
+ - `Effect.fromOption` lifts an Option into an Effect, failing if the Option is `none`.
1075
+ - `Effect.fromEither` lifts an Either into an Effect, failing if the Either is `left`.
1076
+
1077
+ **Anti-Pattern:**
1078
+
1079
+ Passing around `null`, `undefined`, or custom option/either types without converting them, which leads to unsafe, non-composable code and harder error handling.
1080
+
1081
+ **Rationale:**
1082
+
1083
+ Use the `fromNullable`, `fromOption`, and `fromEither` constructors to convert nullable values, `Option`, or `Either` into Effects or Streams.
1084
+ This enables safe, typeful interop with legacy code, APIs, or libraries that use `null`, `undefined`, or their own option/either types.
1085
+
1086
+
1087
+ Converting to Effect, Stream, Option, or Either lets you use all the combinators, error handling, and resource safety of the Effect ecosystem, while avoiding the pitfalls of `null` and `undefined`.
1088
+
1089
+ ---
1090
+
1091
+ ### Create Pre-resolved Effects with succeed and fail
1092
+
1093
+ **Rule:** Create pre-resolved effects with succeed and fail.
1094
+
1095
+ **Good Example:**
1096
+
1097
+ ```typescript
1098
+ import { Effect, Data } from "effect";
1099
+
1100
+ // Create a custom error type
1101
+ class MyError extends Data.TaggedError("MyError") {}
1102
+
1103
+ // Create a program that demonstrates pre-resolved effects
1104
+ const program = Effect.gen(function* () {
1105
+ // Success effect
1106
+ yield* Effect.logInfo("Running success effect...");
1107
+ yield* Effect.gen(function* () {
1108
+ const value = yield* Effect.succeed(42);
1109
+ yield* Effect.logInfo(`Success value: ${value}`);
1110
+ });
1111
+
1112
+ // Failure effect
1113
+ yield* Effect.logInfo("\nRunning failure effect...");
1114
+ yield* Effect.gen(function* () {
1115
+ // Use return yield* for effects that never succeed
1116
+ return yield* Effect.fail(new MyError());
1117
+ }).pipe(
1118
+ Effect.catchTag("MyError", (error) =>
1119
+ Effect.logInfo(`Error occurred: ${error._tag}`)
1120
+ )
1121
+ );
1122
+ });
1123
+
1124
+ // Run the program
1125
+ Effect.runPromise(program);
1126
+ ```
1127
+
1128
+ **Explanation:**
1129
+ Use `Effect.succeed` for values you already have, and `Effect.fail` for
1130
+ immediate, known errors.
1131
+
1132
+ **Anti-Pattern:**
1133
+
1134
+ Do not wrap a static value in `Effect.sync`. While it works, `Effect.succeed`
1135
+ is more descriptive and direct for values that are already available.
1136
+
1137
+ **Rationale:**
1138
+
1139
+ To lift a pure, already-known value into an `Effect`, use `Effect.succeed()`.
1140
+ To represent an immediate and known failure, use `Effect.fail()`.
1141
+
1142
+
1143
+ These are the simplest effect constructors, essential for returning static
1144
+ values within functions that must return an `Effect`.
1145
+
1146
+ ---
1147
+
1148
+ ### Wrapping Synchronous and Asynchronous Computations
1149
+
1150
+ **Rule:** Use try and tryPromise to lift code that may throw or reject into Effect, capturing errors in the failure channel.
1151
+
1152
+ **Good Example:**
1153
+
1154
+ ```typescript
1155
+ import { Effect } from "effect";
1156
+
1157
+ // Synchronous: Wrap code that may throw
1158
+ const effectSync = Effect.try({
1159
+ try: () => JSON.parse("{ invalid json }"),
1160
+ catch: (error) => `Parse error: ${String(error)}`,
1161
+ }); // Effect<string, never, never>
1162
+
1163
+ // Asynchronous: Wrap a promise that may reject
1164
+ const effectAsync = Effect.tryPromise({
1165
+ try: () => fetch("https://api.example.com/data").then((res) => res.json()),
1166
+ catch: (error) => `Network error: ${String(error)}`,
1167
+ }); // Effect<string, any, never>
1168
+ ```
1169
+
1170
+ **Explanation:**
1171
+
1172
+ - `Effect.try` wraps a synchronous computation that may throw, capturing the error in the failure channel.
1173
+ - `Effect.tryPromise` wraps an async computation (Promise) that may reject, capturing the rejection as a failure.
1174
+
1175
+ **Anti-Pattern:**
1176
+
1177
+ Using try/catch for error handling, or relying on untyped Promise rejections, which leads to less composable and less type-safe code.
1178
+
1179
+ **Rationale:**
1180
+
1181
+ Use the `try` and `tryPromise` constructors to safely wrap synchronous or asynchronous computations that may throw exceptions or reject promises.
1182
+ This captures errors in the Effect failure channel, making them type-safe and composable.
1183
+
1184
+
1185
+ Wrapping potentially unsafe code in `try` or `tryPromise` ensures that all errors are handled in a uniform, declarative way.
1186
+ This eliminates the need for try/catch blocks and makes error handling explicit and type-safe.
1187
+
1188
+ ---
1189
+
1190
+ ### Solve Promise Problems with Effect
1191
+
1192
+ **Rule:** Recognize that Effect solves the core limitations of Promises: untyped errors, no dependency injection, and no cancellation.
1193
+
1194
+ **Good Example:**
1195
+
1196
+ This code is type-safe, testable, and cancellable. The signature `Effect.Effect<User, DbError, HttpClient>` tells us everything we need to know.
1197
+
1198
+ ```typescript
1199
+ import { Effect, Data } from "effect";
1200
+
1201
+ interface DbErrorType {
1202
+ readonly _tag: "DbError";
1203
+ readonly message: string;
1204
+ }
1205
+
1206
+ const DbError = Data.tagged<DbErrorType>("DbError");
1207
+
1208
+ interface User {
1209
+ name: string;
1210
+ }
1211
+
1212
+ class HttpClient extends Effect.Service<HttpClient>()("HttpClient", {
1213
+ sync: () => ({
1214
+ findById: (id: number): Effect.Effect<User, DbErrorType> =>
1215
+ Effect.try({
1216
+ try: () => ({ name: `User ${id}` }),
1217
+ catch: () => DbError({ message: "Failed to find user" }),
1218
+ }),
1219
+ }),
1220
+ }) {}
1221
+
1222
+ const findUser = (id: number) =>
1223
+ Effect.gen(function* () {
1224
+ const client = yield* HttpClient;
1225
+ return yield* client.findById(id);
1226
+ });
1227
+
1228
+ // Demonstrate how Effect solves promise problems
1229
+ const program = Effect.gen(function* () {
1230
+ yield* Effect.logInfo("=== Solving Promise Problems with Effect ===");
1231
+
1232
+ // Problem 1: Proper error handling (no more try/catch hell)
1233
+ yield* Effect.logInfo("1. Demonstrating type-safe error handling:");
1234
+
1235
+ const result1 = yield* findUser(123).pipe(
1236
+ Effect.catchAll((error) =>
1237
+ Effect.gen(function* () {
1238
+ yield* Effect.logInfo(`Handled error: ${error.message}`);
1239
+ return { name: "Default User" };
1240
+ })
1241
+ )
1242
+ );
1243
+ yield* Effect.logInfo(`Found user: ${result1.name}`);
1244
+
1245
+ // Problem 2: Easy composition and chaining
1246
+ yield* Effect.logInfo("\n2. Demonstrating easy composition:");
1247
+
1248
+ const composedOperation = Effect.gen(function* () {
1249
+ const user1 = yield* findUser(1);
1250
+ const user2 = yield* findUser(2);
1251
+ yield* Effect.logInfo(`Composed result: ${user1.name} and ${user2.name}`);
1252
+ return [user1, user2];
1253
+ });
1254
+
1255
+ yield* composedOperation;
1256
+
1257
+ // Problem 3: Resource management and cleanup
1258
+ yield* Effect.logInfo("\n3. Demonstrating resource management:");
1259
+
1260
+ const resourceOperation = Effect.gen(function* () {
1261
+ yield* Effect.logInfo("Acquiring resource...");
1262
+ const resource = "database-connection";
1263
+
1264
+ yield* Effect.addFinalizer(() => Effect.logInfo("Cleaning up resource..."));
1265
+
1266
+ const user = yield* findUser(456);
1267
+ yield* Effect.logInfo(`Used resource to get: ${user.name}`);
1268
+
1269
+ return user;
1270
+ }).pipe(Effect.scoped);
1271
+
1272
+ yield* resourceOperation;
1273
+
1274
+ yield* Effect.logInfo("\n✅ All operations completed successfully!");
1275
+ });
1276
+
1277
+ Effect.runPromise(Effect.provide(program, HttpClient.Default));
1278
+ ```
1279
+
1280
+ ---
1281
+
1282
+ **Anti-Pattern:**
1283
+
1284
+ This `Promise`-based function has several hidden problems that Effect solves:
1285
+
1286
+ - What happens if `db.findUser` rejects? The error is untyped (`any`).
1287
+ - Where does `db` come from? It's a hidden dependency, making this function hard to test.
1288
+ - If the operation is slow, how do we cancel it? We can't.
1289
+
1290
+ ```typescript
1291
+ // ❌ This function has hidden dependencies and untyped errors.
1292
+ async function findUserUnsafely(id: number): Promise<any> {
1293
+ try {
1294
+ const user = await db.findUser(id); // `db` is a hidden global or import
1295
+ return user;
1296
+ } catch (error) {
1297
+ // `error` is of type `any`. We don't know what it is.
1298
+ // We might log it and re-throw, but we can't handle it safely.
1299
+ throw error;
1300
+ }
1301
+ }
1302
+ ```
1303
+
1304
+ **Rationale:**
1305
+
1306
+ Recognize that `Effect` is not just a "better Promise," but a fundamentally different construct designed to solve the core limitations of native `Promise`s in TypeScript:
1307
+
1308
+ 1. **Untyped Errors:** Promises can reject with `any` value, forcing `try/catch` blocks and unsafe type checks.
1309
+ 2. **No Dependency Injection:** Promises have no built-in way to declare or manage dependencies, leading to tightly coupled code.
1310
+ 3. **No Cancellation:** Once a `Promise` starts, it cannot be cancelled from the outside.
1311
+
1312
+ ---
1313
+
1314
+
1315
+ While `async/await` is great for simple cases, building large, robust applications with `Promise`s reveals these critical gaps. Effect addresses each one directly:
1316
+
1317
+ - **Typed Errors:** The `E` channel in `Effect<A, E, R>` forces you to handle specific, known error types, eliminating an entire class of runtime bugs.
1318
+ - **Dependency Injection:** The `R` channel provides a powerful, built-in system for declaring and providing dependencies (`Layer`s), making your code modular and testable.
1319
+ - **Cancellation (Interruption):** Effect's structured concurrency and `Fiber` model provide robust, built-in cancellation. When an effect is interrupted, Effect guarantees that its cleanup logic (finalizers) will be run.
1320
+
1321
+ Understanding that Effect was built specifically to solve these problems is key to appreciating its design and power.
1322
+
1323
+ ---
1324
+
1325
+ ---
1326
+
1327
+ ### Creating from Collections
1328
+
1329
+ **Rule:** Use fromIterable and fromArray to lift collections into Streams or Effects for batch or streaming processing.
1330
+
1331
+ **Good Example:**
1332
+
1333
+ ```typescript
1334
+ import { Stream, Effect } from "effect";
1335
+
1336
+ // Stream: Create a stream from an array
1337
+ const numbers = [1, 2, 3, 4];
1338
+ const numberStream = Stream.fromIterable(numbers); // Stream<number>
1339
+
1340
+ // Stream: Create a stream from any iterable
1341
+ function* gen() {
1342
+ yield "a";
1343
+ yield "b";
1344
+ }
1345
+ const letterStream = Stream.fromIterable(gen()); // Stream<string>
1346
+
1347
+ // Effect: Create an effect from an array of effects (batch)
1348
+ const effects = [Effect.succeed(1), Effect.succeed(2)];
1349
+ const batchEffect = Effect.all(effects); // Effect<[1, 2]>
1350
+ ```
1351
+
1352
+ **Explanation:**
1353
+
1354
+ - `Stream.fromIterable` creates a stream from any array or iterable, enabling streaming and batch operations.
1355
+ - `Effect.all` (covered elsewhere) can be used to process arrays of effects in batch.
1356
+
1357
+ **Anti-Pattern:**
1358
+
1359
+ Manually looping over collections and running effects or streams imperatively, which loses composability, error handling, and resource safety.
1360
+
1361
+ **Rationale:**
1362
+
1363
+ Use the `fromIterable` and `fromArray` constructors to create Streams or Effects from arrays, iterables, or other collections.
1364
+ This is the foundation for batch processing, streaming, and working with large or dynamic data sources.
1365
+
1366
+
1367
+ Lifting collections into Streams or Effects allows you to process data in a composable, resource-safe, and potentially concurrent way.
1368
+ It also enables you to use all of Effect's combinators for transformation, filtering, and error handling.
1369
+
1370
+ ---
1371
+
1372
+ ### Working with Tuples using Data.tuple
1373
+
1374
+ **Rule:** Use Data.tuple to define tuples whose equality is based on their contents, enabling safe and predictable comparisons and pattern matching.
1375
+
1376
+ **Good Example:**
1377
+
1378
+ ```typescript
1379
+ import { Data, Equal } from "effect";
1380
+
1381
+ // Create two structurally equal tuples
1382
+ const t1 = Data.tuple(1, "Alice");
1383
+ const t2 = Data.tuple(1, "Alice");
1384
+
1385
+ // Compare by value, not reference
1386
+ const areEqual = Equal.equals(t1, t2); // true
1387
+
1388
+ // Use tuples as keys in a HashSet or Map
1389
+ import { HashSet } from "effect";
1390
+ const set = HashSet.make(t1);
1391
+ console.log(HashSet.has(set, t2)); // true
1392
+
1393
+ // Pattern matching on tuples
1394
+ const [id, name] = t1; // id: number, name: string
1395
+ ```
1396
+
1397
+ **Explanation:**
1398
+
1399
+ - `Data.tuple` creates immutable tuples with value-based equality.
1400
+ - Useful for modeling pairs, coordinates, or any fixed-size, heterogeneous data.
1401
+ - Supports safe pattern matching and collection operations.
1402
+
1403
+ **Anti-Pattern:**
1404
+
1405
+ Using plain arrays for value-based logic or as keys in sets/maps, which compares by reference and can lead to incorrect behavior.
1406
+
1407
+ **Rationale:**
1408
+
1409
+ Use `Data.tuple` to create immutable, type-safe tuples that support value-based equality and pattern matching.
1410
+ This is useful for modeling fixed-size, heterogeneous collections of values in a safe and expressive way.
1411
+
1412
+
1413
+ JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic.
1414
+ `Data.tuple` provides immutable tuples with structural equality, making them ideal for domain modeling and functional programming patterns.
1415
+
1416
+ ---
1417
+
1418
+ ### Lifting Errors and Absence with fail, none, and left
1419
+
1420
+ **Rule:** Use fail, none, and left to create Effect, Option, or Either that represent failure or absence.
1421
+
1422
+ **Good Example:**
1423
+
1424
+ ```typescript
1425
+ import { Effect, Option, Either } from "effect";
1426
+
1427
+ // Effect: Represent a failure with an error value
1428
+ const effect = Effect.fail("Something went wrong"); // Effect<string, never, never>
1429
+
1430
+ // Option: Represent absence of a value
1431
+ const option = Option.none(); // Option<never>
1432
+
1433
+ // Either: Represent a failure with a left value
1434
+ const either = Either.left("Invalid input"); // Either<string, never>
1435
+ ```
1436
+
1437
+ **Explanation:**
1438
+
1439
+ - `Effect.fail(error)` creates an effect that always fails with `error`.
1440
+ - `Option.none()` creates an option that is always absent.
1441
+ - `Either.left(error)` creates an either that always represents failure.
1442
+
1443
+ **Anti-Pattern:**
1444
+
1445
+ Throwing exceptions, returning `null` or `undefined`, or using error codes outside the Effect, Option, or Either world.
1446
+ This makes error handling ad hoc, less type-safe, and harder to compose.
1447
+
1448
+ **Rationale:**
1449
+
1450
+ Use the `fail`, `none`, and `left` constructors to represent errors or absence in the Effect, Option, or Either world.
1451
+ This makes failures explicit, type-safe, and composable.
1452
+
1453
+
1454
+ By lifting errors and absence into these structures, you can handle them declaratively with combinators, rather than relying on exceptions, `null`, or `undefined`.
1455
+ This leads to more robust and maintainable code.
1456
+
1457
+ ---
1458
+
1459
+ ### Wrap Synchronous Computations with sync and try
1460
+
1461
+ **Rule:** Wrap synchronous computations with sync and try.
1462
+
1463
+ **Good Example:**
1464
+
1465
+ ```typescript
1466
+ import { Effect } from "effect";
1467
+
1468
+ const randomNumber = Effect.sync(() => Math.random());
1469
+
1470
+ const parseJson = (input: string) =>
1471
+ Effect.try({
1472
+ try: () => JSON.parse(input),
1473
+ catch: (error) => new Error(`JSON parsing failed: ${error}`),
1474
+ });
1475
+
1476
+ // More examples of wrapping synchronous computations
1477
+ const divide = (a: number, b: number) =>
1478
+ Effect.try({
1479
+ try: () => {
1480
+ if (b === 0) throw new Error("Division by zero");
1481
+ return a / b;
1482
+ },
1483
+ catch: (error) => new Error(`Division failed: ${error}`),
1484
+ });
1485
+
1486
+ const processString = (str: string) =>
1487
+ Effect.gen(function* () {
1488
+ yield* Effect.log(`Processing string: "${str}"`);
1489
+ return str.toUpperCase().split("").reverse().join("");
1490
+ });
1491
+
1492
+ // Demonstrate wrapping synchronous computations
1493
+ const program = Effect.gen(function* () {
1494
+ yield* Effect.log("=== Wrapping Synchronous Computations Demo ===");
1495
+
1496
+ // Example 1: Basic sync computation
1497
+ yield* Effect.log("\n1. Basic sync computation (random number):");
1498
+ const random1 = yield* randomNumber;
1499
+ const random2 = yield* randomNumber;
1500
+ yield* Effect.log(
1501
+ `Random numbers: ${random1.toFixed(4)}, ${random2.toFixed(4)}`
1502
+ );
1503
+
1504
+ // Example 2: Successful JSON parsing
1505
+ yield* Effect.log("\n2. Successful JSON parsing:");
1506
+ const validJson = '{"name": "Paul", "age": 30}';
1507
+ const parsed = yield* parseJson(validJson);
1508
+ yield* Effect.log("Parsed JSON:" + JSON.stringify(parsed));
1509
+
1510
+ // Example 3: Failed JSON parsing with error logging
1511
+ yield* Effect.log("\n3. Failed JSON parsing with error logging:");
1512
+ const invalidJson = '{"name": "Paul", "age":}';
1513
+ yield* parseJson(invalidJson).pipe(
1514
+ Effect.tapError((error) => Effect.log(`Parsing failed: ${error.message}`)),
1515
+ Effect.catchAll(() => Effect.succeed({ name: "default", age: 0 }))
1516
+ );
1517
+ yield* Effect.log("Continued after error (with recovery)");
1518
+
1519
+ // Example 4: Division with error logging and recovery
1520
+ yield* Effect.log("\n4. Division with error logging and recovery:");
1521
+ const division1 = yield* divide(10, 2);
1522
+ yield* Effect.log(`10 / 2 = ${division1}`);
1523
+
1524
+ // Use tapError to log, then catchAll to recover
1525
+ const division2 = yield* divide(10, 0).pipe(
1526
+ Effect.tapError((error) => Effect.log(`Division error: ${error.message}`)),
1527
+ Effect.catchAll(() => Effect.succeed(-1))
1528
+ );
1529
+ yield* Effect.log(`10 / 0 = ${division2} (error handled)`);
1530
+
1531
+ // Example 5: String processing
1532
+ yield* Effect.log("\n5. String processing:");
1533
+ const processed = yield* processString("Hello Effect");
1534
+ yield* Effect.log(`Processed result: "${processed}"`);
1535
+
1536
+ // Example 6: Combining multiple sync operations
1537
+ yield* Effect.log("\n6. Combining multiple sync operations:");
1538
+ const combined = yield* Effect.gen(function* () {
1539
+ const num = yield* randomNumber;
1540
+ const multiplied = yield* Effect.sync(() => num * 100);
1541
+ const rounded = yield* Effect.sync(() => Math.round(multiplied));
1542
+ return rounded;
1543
+ });
1544
+ yield* Effect.log(`Combined operations result: ${combined}`);
1545
+
1546
+ yield* Effect.log("\n✅ Synchronous computations demonstration completed!");
1547
+ });
1548
+
1549
+ Effect.runPromise(program);
1550
+ ```
1551
+
1552
+ **Explanation:**
1553
+ Use `Effect.sync` for safe synchronous code, and `Effect.try` to safely
1554
+ handle exceptions from potentially unsafe code.
1555
+
1556
+ **Anti-Pattern:**
1557
+
1558
+ Never use `Effect.sync` for an operation that could throw, like `JSON.parse`.
1559
+ This can lead to unhandled exceptions that crash your application.
1560
+
1561
+ **Rationale:**
1562
+
1563
+ To bring a synchronous side-effect into Effect, wrap it in a thunk (`() => ...`).
1564
+ Use `Effect.sync` for functions guaranteed not to throw, and `Effect.try` for
1565
+ functions that might throw.
1566
+
1567
+
1568
+ This is the primary way to safely integrate with synchronous libraries like
1569
+ `JSON.parse`. `Effect.try` captures any thrown exception and moves it into
1570
+ the Effect's error channel.
1571
+
1572
+ ---
1573
+
1574
+ ### Use .pipe for Composition
1575
+
1576
+ **Rule:** Use .pipe for composition.
1577
+
1578
+ **Good Example:**
1579
+
1580
+ ```typescript
1581
+ import { Effect } from "effect";
1582
+
1583
+ const program = Effect.succeed(5).pipe(
1584
+ Effect.map((n) => n * 2),
1585
+ Effect.map((n) => `The result is ${n}`),
1586
+ Effect.tap(Effect.log)
1587
+ );
1588
+
1589
+ // Demonstrate various pipe composition patterns
1590
+ const demo = Effect.gen(function* () {
1591
+ yield* Effect.log("=== Using Pipe for Composition Demo ===");
1592
+
1593
+ // 1. Basic pipe composition
1594
+ yield* Effect.log("\n1. Basic pipe composition:");
1595
+ yield* program;
1596
+
1597
+ // 2. Complex pipe composition with multiple transformations
1598
+ yield* Effect.log("\n2. Complex pipe composition:");
1599
+ const complexResult = yield* Effect.succeed(10).pipe(
1600
+ Effect.map((n) => n + 5),
1601
+ Effect.map((n) => n * 2),
1602
+ Effect.tap((n) => Effect.log(`Intermediate result: ${n}`)),
1603
+ Effect.map((n) => n.toString()),
1604
+ Effect.map((s) => `Final: ${s}`)
1605
+ );
1606
+ yield* Effect.log("Complex result: " + complexResult);
1607
+
1608
+ // 3. Pipe with flatMap for chaining effects
1609
+ yield* Effect.log("\n3. Pipe with flatMap for chaining effects:");
1610
+ const chainedResult = yield* Effect.succeed("hello").pipe(
1611
+ Effect.map((s) => s.toUpperCase()),
1612
+ Effect.flatMap((s) => Effect.succeed(`${s} WORLD`)),
1613
+ Effect.flatMap((s) => Effect.succeed(`${s}!​`)),
1614
+ Effect.tap((s) => Effect.log(`Chained: ${s}`))
1615
+ );
1616
+ yield* Effect.log("Chained result: " + chainedResult);
1617
+
1618
+ // 4. Pipe with error handling
1619
+ yield* Effect.log("\n4. Pipe with error handling:");
1620
+ const errorHandledResult = yield* Effect.succeed(-1).pipe(
1621
+ Effect.flatMap((n) =>
1622
+ n > 0 ? Effect.succeed(n) : Effect.fail(new Error("Negative number"))
1623
+ ),
1624
+ Effect.catchAll((error) =>
1625
+ Effect.succeed("Handled error: " + error.message)
1626
+ ),
1627
+ Effect.tap((result) => Effect.log(`Error handled: ${result}`))
1628
+ );
1629
+ yield* Effect.log("Error handled result: " + errorHandledResult);
1630
+
1631
+ // 5. Pipe with multiple operations
1632
+ yield* Effect.log("\n5. Pipe with multiple operations:");
1633
+ const multiOpResult = yield* Effect.succeed([1, 2, 3, 4, 5]).pipe(
1634
+ Effect.map((arr) => arr.filter((n) => n % 2 === 0)),
1635
+ Effect.map((arr) => arr.map((n) => n * 2)),
1636
+ Effect.map((arr) => arr.reduce((sum, n) => sum + n, 0)),
1637
+ Effect.tap((sum) => Effect.log(`Sum of even numbers doubled: ${sum}`))
1638
+ );
1639
+ yield* Effect.log("Multi-operation result: " + multiOpResult);
1640
+
1641
+ yield* Effect.log("\n✅ Pipe composition demonstration completed!");
1642
+ });
1643
+
1644
+ Effect.runPromise(demo);
1645
+ ```
1646
+
1647
+ **Explanation:**
1648
+ Using `.pipe()` allows you to compose operations in a top-to-bottom style,
1649
+ improving readability and maintainability.
1650
+
1651
+ **Anti-Pattern:**
1652
+
1653
+ Nesting function calls manually. This is hard to read and reorder.
1654
+ `Effect.tap(Effect.map(Effect.map(Effect.succeed(5), n => n * 2), n => ...))`
1655
+
1656
+ **Rationale:**
1657
+
1658
+ To apply a sequence of transformations or operations to an `Effect`, use the
1659
+ `.pipe()` method.
1660
+
1661
+
1662
+ Piping makes code readable and avoids deeply nested function calls. It allows
1663
+ you to see the flow of data transformations in a clear, linear fashion.
1664
+
1665
+ ---
1666
+
1667
+ ### Understand the Three Effect Channels (A, E, R)
1668
+
1669
+ **Rule:** Understand that an Effect&lt;A, E, R&gt; describes a computation with a success type (A), an error type (E), and a requirements type (R).
1670
+
1671
+ **Good Example:**
1672
+
1673
+ This function signature is a self-documenting contract. It clearly states that to get a `User`, you must provide a `Database` service, and the operation might fail with a `UserNotFoundError`.
1674
+
1675
+ ```typescript
1676
+ import { Effect, Data } from "effect";
1677
+
1678
+ // Define the types for our channels
1679
+ interface User {
1680
+ readonly name: string;
1681
+ } // The 'A' type
1682
+ class UserNotFoundError extends Data.TaggedError("UserNotFoundError") {} // The 'E' type
1683
+
1684
+ // Define the Database service using Effect.Service
1685
+ export class Database extends Effect.Service<Database>()("Database", {
1686
+ // Provide a default implementation
1687
+ sync: () => ({
1688
+ findUser: (id: number) =>
1689
+ id === 1
1690
+ ? Effect.succeed({ name: "Paul" })
1691
+ : Effect.fail(new UserNotFoundError()),
1692
+ }),
1693
+ }) {}
1694
+
1695
+ // This function's signature shows all three channels
1696
+ const getUser = (
1697
+ id: number
1698
+ ): Effect.Effect<User, UserNotFoundError, Database> =>
1699
+ Effect.gen(function* () {
1700
+ const db = yield* Database;
1701
+ return yield* db.findUser(id);
1702
+ });
1703
+
1704
+ // The program will use the default implementation
1705
+ const program = getUser(1);
1706
+
1707
+ // Run the program with the default implementation
1708
+ const programWithLogging = Effect.gen(function* () {
1709
+ const result = yield* Effect.provide(program, Database.Default);
1710
+ yield* Effect.log(`Result: ${JSON.stringify(result)}`); // { name: 'Paul' }
1711
+ return result;
1712
+ });
1713
+
1714
+ Effect.runPromise(programWithLogging);
1715
+ ```
1716
+
1717
+ ---
1718
+
1719
+ **Anti-Pattern:**
1720
+
1721
+ Ignoring the type system and using generic types. This throws away all the safety and clarity that Effect provides.
1722
+
1723
+ ```typescript
1724
+ import { Effect } from "effect";
1725
+
1726
+ // ❌ WRONG: This signature is dishonest and unsafe.
1727
+ // It hides the dependency on a database and the possibility of failure.
1728
+ function getUserUnsafely(id: number, db: any): Effect.Effect<any> {
1729
+ try {
1730
+ const user = db.findUser(id);
1731
+ if (!user) {
1732
+ // This will be an unhandled defect, not a typed error.
1733
+ throw new Error("User not found");
1734
+ }
1735
+ return Effect.succeed(user);
1736
+ } catch (e) {
1737
+ // This is also an untyped failure.
1738
+ return Effect.fail(e);
1739
+ }
1740
+ }
1741
+ ```
1742
+
1743
+ **Rationale:**
1744
+
1745
+ Every `Effect` has three generic type parameters: `Effect<A, E, R>` which represent its three "channels":
1746
+
1747
+ - **`A` (Success Channel):** The type of value the `Effect` will produce if it succeeds.
1748
+ - **`E` (Error/Failure Channel):** The type of error the `Effect` can fail with. These are expected, recoverable errors.
1749
+ - **`R` (Requirement/Context Channel):** The services or dependencies the `Effect` needs to run.
1750
+
1751
+ ---
1752
+
1753
+
1754
+ This three-channel signature is what makes Effect so expressive and safe. Unlike a `Promise<A>` which can only describe its success type, an `Effect`'s signature tells you everything you need to know about a computation before you run it:
1755
+
1756
+ 1. **What it produces (`A`):** The data you get on the "happy path."
1757
+ 2. **How it can fail (`E`):** The specific, known errors you need to handle. This makes error handling type-safe and explicit, unlike throwing generic `Error`s.
1758
+ 3. **What it needs (`R`):** The "ingredients" or dependencies required to run the effect. This is the foundation of Effect's powerful dependency injection system. An `Effect` can only be executed when its `R` channel is `never`, meaning all its dependencies have been provided.
1759
+
1760
+ This turns the TypeScript compiler into a powerful assistant that ensures you've handled all possible outcomes and provided all necessary dependencies.
1761
+
1762
+ ---
1763
+
1764
+ ---
1765
+
1766
+ ### Working with Immutable Arrays using Data.array
1767
+
1768
+ **Rule:** Use Data.array to define arrays whose equality is based on their contents, enabling safe, predictable comparisons and functional operations.
1769
+
1770
+ **Good Example:**
1771
+
1772
+ ```typescript
1773
+ import { Data, Equal } from "effect";
1774
+
1775
+ // Create two structurally equal arrays
1776
+ const arr1 = Data.array([1, 2, 3]);
1777
+ const arr2 = Data.array([1, 2, 3]);
1778
+
1779
+ // Compare by value, not reference
1780
+ const areEqual = Equal.equals(arr1, arr2); // true
1781
+
1782
+ // Use arrays as keys in a HashSet or Map
1783
+ import { HashSet } from "effect";
1784
+ const set = HashSet.make(arr1);
1785
+ console.log(HashSet.has(set, arr2)); // true
1786
+
1787
+ // Functional operations (map, filter, etc.)
1788
+ const doubled = arr1.map((n) => n * 2); // Data.array([2, 4, 6])
1789
+ ```
1790
+
1791
+ **Explanation:**
1792
+
1793
+ - `Data.array` creates immutable arrays with value-based equality.
1794
+ - Useful for modeling ordered collections in a safe, functional way.
1795
+ - Supports all standard array operations, but with immutability and structural equality.
1796
+
1797
+ **Anti-Pattern:**
1798
+
1799
+ Using plain JavaScript arrays for value-based logic, as keys in sets/maps, or in concurrent code, which can lead to bugs due to mutability and reference-based comparison.
1800
+
1801
+ **Rationale:**
1802
+
1803
+ Use `Data.array` to create immutable, type-safe arrays that support value-based equality and safe functional operations.
1804
+ This is useful for modeling ordered collections where immutability and structural equality are important.
1805
+
1806
+
1807
+ JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic and concurrent code.
1808
+ `Data.array` provides immutable arrays with structural equality, making them ideal for functional programming and safe domain modeling.
1809
+
1810
+ ---
1811
+
1812
+
1813
+ ## 🟡 Intermediate Patterns
1814
+
1815
+ ### Representing Time Spans with Duration
1816
+
1817
+ **Rule:** Use Duration to model and manipulate time spans, enabling safe and expressive time-based logic.
1818
+
1819
+ **Good Example:**
1820
+
1821
+ ```typescript
1822
+ import { Duration } from "effect";
1823
+
1824
+ // Create durations using helpers
1825
+ const oneSecond = Duration.seconds(1);
1826
+ const fiveMinutes = Duration.minutes(5);
1827
+ const twoHours = Duration.hours(2);
1828
+
1829
+ // Add, subtract, and compare durations
1830
+ const total = Duration.sum(oneSecond, fiveMinutes); // 5 min 1 sec
1831
+ const isLonger = Duration.greaterThan(twoHours, fiveMinutes); // true
1832
+
1833
+ // Convert to milliseconds or ISO string
1834
+ const ms = Duration.toMillis(fiveMinutes); // 300000
1835
+ const iso = Duration.formatIso(oneSecond); // "PT1S"
1836
+ ```
1837
+
1838
+ **Explanation:**
1839
+
1840
+ - `Duration` is immutable and type-safe.
1841
+ - Use helpers for common intervals and arithmetic for composition.
1842
+ - Prefer `Duration` over raw numbers for all time-based logic.
1843
+
1844
+ **Anti-Pattern:**
1845
+
1846
+ Using raw numbers (e.g., `5000` for 5 seconds) for time intervals, which is error-prone, hard to read, and less maintainable.
1847
+
1848
+ **Rationale:**
1849
+
1850
+ Use the `Duration` data type to represent and manipulate time intervals in a type-safe, human-readable, and composable way.
1851
+ This enables robust time-based logic for scheduling, retries, timeouts, and more.
1852
+
1853
+
1854
+ Working with raw numbers for time intervals (e.g., milliseconds) is error-prone and hard to read.
1855
+ `Duration` provides a clear, expressive API for modeling time spans, improving code safety and maintainability.
1856
+
1857
+ ---
1858
+
1859
+ ### Use Chunk for High-Performance Collections
1860
+
1861
+ **Rule:** Use Chunk to model immutable, high-performance collections for efficient data processing and transformation.
1862
+
1863
+ **Good Example:**
1864
+
1865
+ ```typescript
1866
+ import { Chunk } from "effect";
1867
+
1868
+ // Create a Chunk from an array
1869
+ const numbers = Chunk.fromIterable([1, 2, 3, 4]); // Chunk<number>
1870
+
1871
+ // Map and filter over a Chunk
1872
+ const doubled = Chunk.map(numbers, (n) => n * 2); // Chunk<number>
1873
+ const evens = Chunk.filter(numbers, (n) => n % 2 === 0); // Chunk<number>
1874
+
1875
+ // Concatenate Chunks
1876
+ const moreNumbers = Chunk.fromIterable([5, 6]);
1877
+ const allNumbers = Chunk.appendAll(numbers, moreNumbers); // Chunk<number>
1878
+
1879
+ // Convert back to array
1880
+ const arr = Chunk.toArray(allNumbers); // number[]
1881
+ ```
1882
+
1883
+ **Explanation:**
1884
+
1885
+ - `Chunk` is immutable and optimized for performance.
1886
+ - It supports efficient batch operations, concatenation, and transformation.
1887
+ - Use `Chunk` in data pipelines, streaming, and concurrent scenarios.
1888
+
1889
+ **Anti-Pattern:**
1890
+
1891
+ Using mutable JavaScript arrays for shared or concurrent data, or for large-scale data processing, which can lead to bugs, inefficiency, and unpredictable behavior.
1892
+
1893
+ **Rationale:**
1894
+
1895
+ Use the `Chunk<A>` data type as an immutable, high-performance alternative to JavaScript's `Array`.
1896
+ `Chunk` is optimized for functional programming, batch processing, and streaming scenarios.
1897
+
1898
+
1899
+ `Chunk` provides efficient, immutable operations for large or frequently transformed collections.
1900
+ It avoids the pitfalls of mutable arrays and is designed for use in concurrent and streaming workflows.
1901
+
1902
+ ---
1903
+
1904
+ ### Work with Immutable Sets using HashSet
1905
+
1906
+ **Rule:** Use HashSet to represent sets of unique values with efficient, immutable operations for membership, union, intersection, and difference.
1907
+
1908
+ **Good Example:**
1909
+
1910
+ ```typescript
1911
+ import { HashSet } from "effect";
1912
+
1913
+ // Create a HashSet from an array
1914
+ const setA = HashSet.fromIterable([1, 2, 3]);
1915
+ const setB = HashSet.fromIterable([3, 4, 5]);
1916
+
1917
+ // Membership check
1918
+ const hasTwo = HashSet.has(setA, 2); // true
1919
+
1920
+ // Union, intersection, difference
1921
+ const union = HashSet.union(setA, setB); // HashSet {1, 2, 3, 4, 5}
1922
+ const intersection = HashSet.intersection(setA, setB); // HashSet {3}
1923
+ const difference = HashSet.difference(setA, setB); // HashSet {1, 2}
1924
+
1925
+ // Add and remove elements
1926
+ const withSix = HashSet.add(setA, 6); // HashSet {1, 2, 3, 6}
1927
+ const withoutOne = HashSet.remove(setA, 1); // HashSet {2, 3}
1928
+ ```
1929
+
1930
+ **Explanation:**
1931
+
1932
+ - `HashSet` is immutable and supports efficient set operations.
1933
+ - Use it for membership checks, set algebra, and modeling unique collections.
1934
+ - Safe for concurrent and functional workflows.
1935
+
1936
+ **Anti-Pattern:**
1937
+
1938
+ Using mutable JavaScript `Set` for shared or concurrent data, or for set operations in functional code, which can lead to bugs and unpredictable behavior.
1939
+
1940
+ **Rationale:**
1941
+
1942
+ Use the `HashSet<A>` data type to represent sets of unique values with efficient, immutable operations.
1943
+ `HashSet` is ideal for membership checks, set algebra, and modeling collections where uniqueness matters.
1944
+
1945
+
1946
+ `HashSet` provides high-performance, immutable set operations that are safe for concurrent and functional programming.
1947
+ It avoids the pitfalls of mutable JavaScript `Set` and is optimized for use in Effect workflows.
1948
+
1949
+ ---
1950
+
1951
+ ### Sequencing with andThen, tap, and flatten
1952
+
1953
+ **Rule:** Use sequencing combinators to run computations in order, perform side effects, or flatten nested structures, while preserving error and context handling.
1954
+
1955
+ **Good Example:**
1956
+
1957
+ ```typescript
1958
+ import { Effect, Stream, Option, Either } from "effect";
1959
+
1960
+ // andThen: Run one effect, then another, ignore the first result
1961
+ const logThenCompute = Effect.log("Starting...").pipe(
1962
+ Effect.andThen(Effect.succeed(42))
1963
+ ); // Effect<number>
1964
+
1965
+ // tap: Log the result of an effect, but keep the value
1966
+ const computeAndLog = Effect.succeed(42).pipe(
1967
+ Effect.tap((n) => Effect.log(`Result is ${n}`))
1968
+ ); // Effect<number>
1969
+
1970
+ // flatten: Remove one level of nesting
1971
+ const nestedOption = Option.some(Option.some(1));
1972
+ const flatOption = Option.flatten(nestedOption); // Option<number>
1973
+
1974
+ const nestedEffect = Effect.succeed(Effect.succeed(1));
1975
+ const flatEffect = Effect.flatten(nestedEffect); // Effect<number>
1976
+
1977
+ // tapError: Log errors without handling them
1978
+ const mightFail = Effect.fail("fail!").pipe(
1979
+ Effect.tapError((err) => Effect.logError(`Error: ${err}`))
1980
+ ); // Effect<never>
1981
+
1982
+ // Stream: tap for side effects on each element
1983
+ const stream = Stream.fromIterable([1, 2, 3]).pipe(
1984
+ Stream.tap((n) => Effect.log(`Saw: ${n}`))
1985
+ ); // Stream<number>
1986
+ ```
1987
+
1988
+ **Explanation:**
1989
+
1990
+ - `andThen` is for sequencing when you don’t care about the first result.
1991
+ - `tap` is for running side effects (like logging) without changing the value.
1992
+ - `flatten` is for removing unnecessary nesting (e.g., `Option<Option<A>>` → `Option<A>`).
1993
+
1994
+ **Anti-Pattern:**
1995
+
1996
+ Using `flatMap` with a function that ignores its argument, or manually unwrapping and re-wrapping nested structures, instead of using the dedicated combinators.
1997
+
1998
+ **Rationale:**
1999
+
2000
+ Use sequencing combinators to run computations in order, perform side effects, or flatten nested structures.
2001
+
2002
+ - `andThen` runs one computation after another, ignoring the first result.
2003
+ - `tap` runs a side-effecting computation with the result, without changing the value.
2004
+ - `flatten` removes one level of nesting from nested structures.
2005
+
2006
+ These work for `Effect`, `Stream`, `Option`, and `Either`.
2007
+
2008
+
2009
+ Sequencing is fundamental for expressing workflows.
2010
+ These combinators let you:
2011
+
2012
+ - Run computations in order (`andThen`)
2013
+ - Attach logging, metrics, or other side effects (`tap`)
2014
+ - Simplify nested structures (`flatten`)
2015
+
2016
+ All while preserving composability, error handling, and type safety.
2017
+
2018
+ ---
2019
+
2020
+ ### Handling Errors with catchAll, orElse, and match
2021
+
2022
+ **Rule:** Use error handling combinators to recover from failures, provide fallback values, or transform errors in a composable way.
2023
+
2024
+ **Good Example:**
2025
+
2026
+ ```typescript
2027
+ import { Effect, Option, Either } from "effect";
2028
+
2029
+ // Effect: Recover from any error
2030
+ const effect = Effect.fail("fail!").pipe(
2031
+ Effect.catchAll((err) => Effect.succeed(`Recovered from: ${err}`))
2032
+ ); // Effect<string>
2033
+
2034
+ // Option: Provide a fallback if value is None
2035
+ const option = Option.none().pipe(Option.orElse(() => Option.some("default"))); // Option<string>
2036
+
2037
+ // Either: Provide a fallback if value is Left
2038
+ const either = Either.left("error").pipe(
2039
+ Either.orElse(() => Either.right("fallback"))
2040
+ ); // Either<never, string>
2041
+
2042
+ // Effect: Pattern match on success or failure
2043
+ const matchEffect = Effect.fail("fail!").pipe(
2044
+ Effect.match({
2045
+ onFailure: (err) => `Error: ${err}`,
2046
+ onSuccess: (value) => `Success: ${value}`,
2047
+ })
2048
+ ); // Effect<string>
2049
+ ```
2050
+
2051
+ **Explanation:**
2052
+ These combinators let you handle errors, provide defaults, or transform error values in a way that is composable and type-safe.
2053
+ You can recover from errors, provide alternative computations, or pattern match on success/failure.
2054
+
2055
+ **Anti-Pattern:**
2056
+
2057
+ Using try/catch, null checks, or imperative error handling outside the combinator world.
2058
+ This breaks composability, loses type safety, and makes error propagation unpredictable.
2059
+
2060
+ **Rationale:**
2061
+
2062
+ Use combinators like `catchAll`, `orElse`, and `match` to handle errors declaratively.
2063
+ These allow you to recover from failures, provide fallback values, or transform errors, all while preserving composability and type safety.
2064
+
2065
+
2066
+ Error handling is a first-class concern in functional programming.
2067
+ By using combinators, you keep error recovery logic close to where errors may occur, and avoid scattering try/catch or null checks throughout your code.
2068
+
2069
+ ---
2070
+
2071
+ ### Access Configuration from the Context
2072
+
2073
+ **Rule:** Access configuration from the Effect context.
2074
+
2075
+ **Good Example:**
2076
+
2077
+ ```typescript
2078
+ import { Config, Effect, Layer } from "effect";
2079
+
2080
+ // Define config service
2081
+ class AppConfig extends Effect.Service<AppConfig>()("AppConfig", {
2082
+ sync: () => ({
2083
+ host: "localhost",
2084
+ port: 3000,
2085
+ }),
2086
+ }) {}
2087
+
2088
+ // Create program that uses config
2089
+ const program = Effect.gen(function* () {
2090
+ const config = yield* AppConfig;
2091
+ yield* Effect.log(`Starting server on http://${config.host}:${config.port}`);
2092
+ });
2093
+
2094
+ // Run the program with default config
2095
+ Effect.runPromise(Effect.provide(program, AppConfig.Default));
2096
+ ```
2097
+
2098
+ **Explanation:**
2099
+ By yielding the config object, you make your dependency explicit and leverage Effect's context system for testability and modularity.
2100
+
2101
+ **Anti-Pattern:**
2102
+
2103
+ Passing configuration values down through multiple function arguments ("prop-drilling"). This is cumbersome and obscures which components truly need which values.
2104
+
2105
+ **Rationale:**
2106
+
2107
+ Inside an `Effect.gen` block, use `yield*` on your `Config` object to access the resolved, type-safe configuration values from the context.
2108
+
2109
+
2110
+ This allows your business logic to declaratively state its dependency on a piece of configuration. The logic is clean, type-safe, and completely decoupled from _how_ the configuration is provided.
2111
+
2112
+ ---
2113
+
2114
+ ### Redact and Handle Sensitive Data
2115
+
2116
+ **Rule:** Use Redacted to wrap sensitive values, preventing accidental exposure in logs or error messages.
2117
+
2118
+ **Good Example:**
2119
+
2120
+ ```typescript
2121
+ import { Redacted } from "effect";
2122
+
2123
+ // Wrap a sensitive value
2124
+ const secret = Redacted.make("super-secret-password");
2125
+
2126
+ // Use the secret in your application logic
2127
+ function authenticate(user: string, password: Redacted.Redacted<string>) {
2128
+ // ... authentication logic
2129
+ }
2130
+
2131
+ // Logging or stringifying a Redacted value
2132
+ console.log(`Password: ${secret}`); // Output: Password: <redacted>
2133
+ console.log(String(secret)); // Output: <redacted>
2134
+ ```
2135
+
2136
+ **Explanation:**
2137
+
2138
+ - `Redacted.make(value)` wraps a sensitive value.
2139
+ - When logged or stringified, the value is replaced with `<redacted>`.
2140
+ - Prevents accidental exposure of secrets in logs or error messages.
2141
+
2142
+ **Anti-Pattern:**
2143
+
2144
+ Passing sensitive data as plain strings, which can be accidentally logged, serialized, or leaked in error messages.
2145
+
2146
+ **Rationale:**
2147
+
2148
+ Use the `Redacted` data type to securely handle sensitive data such as passwords, API keys, or tokens.
2149
+ `Redacted` ensures that secrets are not accidentally logged, serialized, or exposed in error messages.
2150
+
2151
+
2152
+ Sensitive data should never appear in logs, traces, or error messages.
2153
+ `Redacted` provides a type-safe way to mark and protect secrets throughout your application.
2154
+
2155
+ ---
2156
+
2157
+ ### Modeling Effect Results with Exit
2158
+
2159
+ **Rule:** Use Exit to capture the outcome of an Effect, including success, failure, and defects, for robust error handling and coordination.
2160
+
2161
+ **Good Example:**
2162
+
2163
+ ```typescript
2164
+ import { Effect, Exit } from "effect";
2165
+
2166
+ // Run an Effect and capture its Exit value
2167
+ const program = Effect.succeed(42);
2168
+
2169
+ const runAndCapture = Effect.runPromiseExit(program); // Promise<Exit<never, number>>
2170
+
2171
+ // Pattern match on Exit
2172
+ runAndCapture.then((exit) => {
2173
+ if (Exit.isSuccess(exit)) {
2174
+ console.log("Success:", exit.value);
2175
+ } else if (Exit.isFailure(exit)) {
2176
+ console.error("Failure:", exit.cause);
2177
+ }
2178
+ });
2179
+ ```
2180
+
2181
+ **Explanation:**
2182
+
2183
+ - `Exit` captures both success (`Exit.success(value)`) and failure (`Exit.failure(cause)`).
2184
+ - Use `Exit` for robust error handling, supervision, and coordination of concurrent effects.
2185
+ - Pattern matching on `Exit` lets you handle all possible outcomes.
2186
+
2187
+ **Anti-Pattern:**
2188
+
2189
+ Ignoring the outcome of an effect, or only handling success/failure without distinguishing between error types or defects, which can lead to missed errors and less robust code.
2190
+
2191
+ **Rationale:**
2192
+
2193
+ Use the `Exit<E, A>` data type to represent the result of running an `Effect`, capturing both success and failure (including defects) in a type-safe way.
2194
+ `Exit` is especially useful for coordinating concurrent workflows and robust error handling.
2195
+
2196
+
2197
+ When running or supervising effects, you often need to know not just if they succeeded or failed, but _how_ they failed (e.g., error vs. defect).
2198
+ `Exit` provides a complete, type-safe summary of an effect's outcome.
2199
+
2200
+ ---
2201
+
2202
+ ### Work with Arbitrary-Precision Numbers using BigDecimal
2203
+
2204
+ **Rule:** Use BigDecimal to represent and compute with decimal numbers that require arbitrary precision, such as in finance or scientific domains.
2205
+
2206
+ **Good Example:**
2207
+
2208
+ ```typescript
2209
+ import { BigDecimal } from "effect";
2210
+
2211
+ // Create BigDecimal values
2212
+ const a = BigDecimal.fromNumber(0.1);
2213
+ const b = BigDecimal.fromNumber(0.2);
2214
+
2215
+ // Add, subtract, multiply, divide
2216
+ const sum = BigDecimal.sum(a, b); // BigDecimal(0.3)
2217
+ const product = BigDecimal.multiply(a, b); // BigDecimal(0.02)
2218
+
2219
+ // Compare values
2220
+ const isEqual = BigDecimal.equals(sum, BigDecimal.fromNumber(0.3)); // true
2221
+
2222
+ // Convert to string or number
2223
+ const asString = BigDecimal.format(BigDecimal.normalize(sum)); // "0.3"
2224
+ const asNumber = BigDecimal.unsafeToNumber(sum); // 0.3
2225
+ ```
2226
+
2227
+ **Explanation:**
2228
+
2229
+ - `BigDecimal` is immutable and supports precise decimal arithmetic.
2230
+ - Use it for domains where rounding errors are unacceptable (e.g., finance, billing, scientific data).
2231
+ - Avoids the pitfalls of floating-point math in JavaScript.
2232
+
2233
+ **Anti-Pattern:**
2234
+
2235
+ Using JavaScript's native `number` type for financial or scientific calculations, which can lead to rounding errors and loss of precision.
2236
+
2237
+ **Rationale:**
2238
+
2239
+ Use the `BigDecimal` data type for decimal numbers that require arbitrary precision, such as financial or scientific calculations.
2240
+ This avoids rounding errors and loss of precision that can occur with JavaScript's native `number` type.
2241
+
2242
+
2243
+ JavaScript's `number` type is a floating-point double, which can introduce subtle bugs in calculations that require exact decimal representation.
2244
+ `BigDecimal` provides precise, immutable arithmetic for critical domains.
2245
+
2246
+ ---
2247
+
2248
+ ### Representing Time Spans with Duration
2249
+
2250
+ **Rule:** Use the Duration data type to represent time intervals instead of raw numbers.
2251
+
2252
+ **Good Example:**
2253
+
2254
+ This example shows how to create and use `Duration` to make time-based operations clear and unambiguous.
2255
+
2256
+ ```typescript
2257
+ import { Effect, Duration } from "effect";
2258
+
2259
+ // Create durations with clear, explicit units
2260
+ const fiveSeconds = Duration.seconds(5);
2261
+ const oneHundredMillis = Duration.millis(100);
2262
+
2263
+ // Use them in Effect operators
2264
+ const program = Effect.log("Starting...").pipe(
2265
+ Effect.delay(oneHundredMillis),
2266
+ Effect.flatMap(() => Effect.log("Running after 100ms")),
2267
+ Effect.timeout(fiveSeconds) // This whole operation must complete within 5 seconds
2268
+ );
2269
+
2270
+ // Durations can also be compared
2271
+ const isLonger = Duration.greaterThan(fiveSeconds, oneHundredMillis); // true
2272
+
2273
+ // Demonstrate the duration functionality
2274
+ const demonstration = Effect.gen(function* () {
2275
+ yield* Effect.logInfo("=== Duration Demonstration ===");
2276
+
2277
+ // Show duration values
2278
+ yield* Effect.logInfo(`Five seconds: ${Duration.toMillis(fiveSeconds)}ms`);
2279
+ yield* Effect.logInfo(
2280
+ `One hundred millis: ${Duration.toMillis(oneHundredMillis)}ms`
2281
+ );
2282
+
2283
+ // Show comparison
2284
+ yield* Effect.logInfo(`Is 5 seconds longer than 100ms? ${isLonger}`);
2285
+
2286
+ // Run the timed program
2287
+ yield* Effect.logInfo("Running timed program...");
2288
+ yield* program;
2289
+
2290
+ // Show more duration operations
2291
+ const combined = Duration.sum(fiveSeconds, oneHundredMillis);
2292
+ yield* Effect.logInfo(`Combined duration: ${Duration.toMillis(combined)}ms`);
2293
+
2294
+ // Show different duration units
2295
+ const oneMinute = Duration.minutes(1);
2296
+ yield* Effect.logInfo(`One minute: ${Duration.toMillis(oneMinute)}ms`);
2297
+
2298
+ const isMinuteLonger = Duration.greaterThan(oneMinute, fiveSeconds);
2299
+ yield* Effect.logInfo(`Is 1 minute longer than 5 seconds? ${isMinuteLonger}`);
2300
+ });
2301
+
2302
+ Effect.runPromise(demonstration);
2303
+ ```
2304
+
2305
+ ---
2306
+
2307
+ **Anti-Pattern:**
2308
+
2309
+ Using raw numbers for time-based operations. This is ambiguous and error-prone.
2310
+
2311
+ ```typescript
2312
+ import { Effect } from "effect";
2313
+
2314
+ // ❌ WRONG: What does '2000' mean? Milliseconds? Seconds?
2315
+ const program = Effect.log("Waiting...").pipe(Effect.delay(2000));
2316
+
2317
+ // This is especially dangerous when different parts of an application
2318
+ // use different conventions (e.g., one service uses seconds, another uses milliseconds).
2319
+ // Using Duration eliminates this entire class of bugs.
2320
+ ```
2321
+
2322
+ **Rationale:**
2323
+
2324
+ When you need to represent a span of time (e.g., for a delay, timeout, or schedule), use the `Duration` data type. Create durations with expressive constructors like `Duration.seconds(5)`, `Duration.minutes(10)`, or `Duration.millis(500)`.
2325
+
2326
+ ---
2327
+
2328
+
2329
+ Using raw numbers to represent time is a common source of bugs and confusion. When you see `setTimeout(fn, 5000)`, it's not immediately clear if the unit is seconds or milliseconds without prior knowledge of the API.
2330
+
2331
+ `Duration` solves this by making the unit explicit in the code. It provides a type-safe, immutable, and human-readable way to work with time intervals. This eliminates ambiguity and makes your code easier to read and maintain. Durations are used throughout Effect's time-based operators, such as `Effect.sleep`, `Effect.timeout`, and `Schedule`.
2332
+
2333
+ ---
2334
+
2335
+ ---
2336
+
2337
+ ### Control Flow with Conditional Combinators
2338
+
2339
+ **Rule:** Use conditional combinators for control flow.
2340
+
2341
+ **Good Example:**
2342
+
2343
+ ```typescript
2344
+ import { Effect } from "effect";
2345
+
2346
+ const attemptAdminAction = (user: { isAdmin: boolean }) =>
2347
+ Effect.if(user.isAdmin, {
2348
+ onTrue: () => Effect.succeed("Admin action completed."),
2349
+ onFalse: () => Effect.fail("Permission denied."),
2350
+ });
2351
+
2352
+ const program = Effect.gen(function* () {
2353
+ // Try with admin user
2354
+ yield* Effect.logInfo("\nTrying with admin user...");
2355
+ const adminResult = yield* Effect.either(
2356
+ attemptAdminAction({ isAdmin: true })
2357
+ );
2358
+ yield* Effect.logInfo(
2359
+ `Admin result: ${adminResult._tag === "Right" ? adminResult.right : adminResult.left}`
2360
+ );
2361
+
2362
+ // Try with non-admin user
2363
+ yield* Effect.logInfo("\nTrying with non-admin user...");
2364
+ const userResult = yield* Effect.either(
2365
+ attemptAdminAction({ isAdmin: false })
2366
+ );
2367
+ yield* Effect.logInfo(
2368
+ `User result: ${userResult._tag === "Right" ? userResult.right : userResult.left}`
2369
+ );
2370
+ });
2371
+
2372
+ Effect.runPromise(program);
2373
+ ```
2374
+
2375
+ **Explanation:**
2376
+ `Effect.if` and related combinators allow you to branch logic without leaving
2377
+ the Effect world or breaking the flow of composition.
2378
+
2379
+ **Anti-Pattern:**
2380
+
2381
+ Using `Effect.gen` for a single, simple conditional check can be more verbose
2382
+ than necessary. For simple branching, `Effect.if` is often more concise.
2383
+
2384
+ **Rationale:**
2385
+
2386
+ Use declarative combinators like `Effect.if`, `Effect.when`, and
2387
+ `Effect.unless` to execute effects based on runtime conditions.
2388
+
2389
+
2390
+ These combinators allow you to embed conditional logic directly into your
2391
+ `.pipe()` compositions, maintaining a declarative style for simple branching.
2392
+
2393
+ ---
2394
+
2395
+ ### Process Streaming Data with Stream
2396
+
2397
+ **Rule:** Use Stream to model and process data that arrives over time in a composable, efficient way.
2398
+
2399
+ **Good Example:**
2400
+
2401
+ This example demonstrates creating a `Stream` from a paginated API. The `Stream` will make API calls as needed, processing one page of users at a time without ever holding the entire user list in memory.
2402
+
2403
+ ```typescript
2404
+ import { Effect, Stream, Option } from "effect";
2405
+
2406
+ interface User {
2407
+ id: number;
2408
+ name: string;
2409
+ }
2410
+ interface PaginatedResponse {
2411
+ users: User[];
2412
+ nextPage: number | null;
2413
+ }
2414
+
2415
+ // A mock API call that returns a page of users
2416
+ const fetchUserPage = (
2417
+ page: number
2418
+ ): Effect.Effect<PaginatedResponse, "ApiError"> =>
2419
+ Effect.succeed(
2420
+ page < 3
2421
+ ? {
2422
+ users: [
2423
+ { id: page * 2 + 1, name: `User ${page * 2 + 1}` },
2424
+ { id: page * 2 + 2, name: `User ${page * 2 + 2}` },
2425
+ ],
2426
+ nextPage: page + 1,
2427
+ }
2428
+ : { users: [], nextPage: null }
2429
+ ).pipe(Effect.delay("50 millis"));
2430
+
2431
+ // Stream.paginateEffect creates a stream from a paginated source
2432
+ const userStream: Stream.Stream<User, "ApiError"> = Stream.paginateEffect(
2433
+ 0,
2434
+ (page) =>
2435
+ fetchUserPage(page).pipe(
2436
+ Effect.map(
2437
+ (response) =>
2438
+ [response.users, Option.fromNullable(response.nextPage)] as const
2439
+ )
2440
+ )
2441
+ ).pipe(
2442
+ // Flatten the stream of user arrays into a stream of individual users
2443
+ Stream.flatMap((users) => Stream.fromIterable(users))
2444
+ );
2445
+
2446
+ // We can now process the stream of users.
2447
+ // Stream.runForEach will pull from the stream until it's exhausted.
2448
+ const program = Stream.runForEach(userStream, (user: User) =>
2449
+ Effect.log(`Processing user: ${user.name}`)
2450
+ );
2451
+
2452
+ const programWithErrorHandling = program.pipe(
2453
+ Effect.catchAll((error) =>
2454
+ Effect.gen(function* () {
2455
+ yield* Effect.logError(`Stream processing error: ${error}`);
2456
+ return null;
2457
+ })
2458
+ )
2459
+ );
2460
+
2461
+ Effect.runPromise(programWithErrorHandling);
2462
+ ```
2463
+
2464
+ ---
2465
+
2466
+ **Anti-Pattern:**
2467
+
2468
+ Manually managing pagination state with recursive functions. This is complex, stateful, and easy to get wrong. It also requires loading all results into memory, which is inefficient for large datasets.
2469
+
2470
+ ```typescript
2471
+ import { Effect } from "effect";
2472
+ import { fetchUserPage } from "./somewhere"; // From previous example
2473
+
2474
+ // ❌ WRONG: Manual, stateful, and inefficient recursion.
2475
+ const fetchAllUsers = (
2476
+ page: number,
2477
+ acc: any[]
2478
+ ): Effect.Effect<any[], "ApiError"> =>
2479
+ fetchUserPage(page).pipe(
2480
+ Effect.flatMap((response) => {
2481
+ const allUsers = [...acc, ...response.users];
2482
+ if (response.nextPage) {
2483
+ return fetchAllUsers(response.nextPage, allUsers);
2484
+ }
2485
+ return Effect.succeed(allUsers);
2486
+ })
2487
+ );
2488
+
2489
+ // This holds all users in memory at once.
2490
+ const program = fetchAllUsers(0, []);
2491
+ ```
2492
+
2493
+ **Rationale:**
2494
+
2495
+ When dealing with a sequence of data that arrives asynchronously, model it as a `Stream`. A `Stream<A, E, R>` is like an asynchronous, effectful `Array`. It represents a sequence of values of type `A` that may fail with an error `E` and requires services `R`.
2496
+
2497
+ ---
2498
+
2499
+
2500
+ Some data sources don't fit the one-shot request/response model of `Effect`. For example:
2501
+
2502
+ - Reading a multi-gigabyte file from disk.
2503
+ - Receiving messages from a WebSocket.
2504
+ - Fetching results from a paginated API.
2505
+
2506
+ Loading all this data into memory at once would be inefficient or impossible. `Stream` solves this by allowing you to process the data in chunks as it arrives. It provides a rich API of composable operators (`map`, `filter`, `run`, etc.) that mirror those on `Effect` and `Array`, but are designed for streaming data. This allows you to build efficient, constant-memory data processing pipelines.
2507
+
2508
+ ---
2509
+
2510
+ ---
2511
+
2512
+ ### Understand Layers for Dependency Injection
2513
+
2514
+ **Rule:** Understand that a Layer is a blueprint describing how to construct a service and its dependencies.
2515
+
2516
+ **Good Example:**
2517
+
2518
+ Here, we define a `Notifier` service that requires a `Logger` to be built. The `NotifierLive` layer's type signature, `Layer<Logger, never, Notifier>`, clearly documents this dependency.
2519
+
2520
+ ```typescript
2521
+ import { Effect } from "effect";
2522
+
2523
+ // Define the Logger service with a default implementation
2524
+ export class Logger extends Effect.Service<Logger>()("Logger", {
2525
+ // Provide a synchronous implementation
2526
+ sync: () => ({
2527
+ log: (msg: string) => Effect.log(`LOG: ${msg}`),
2528
+ }),
2529
+ }) {}
2530
+
2531
+ // Define the Notifier service that depends on Logger
2532
+ export class Notifier extends Effect.Service<Notifier>()("Notifier", {
2533
+ // Provide an implementation that requires Logger
2534
+ effect: Effect.gen(function* () {
2535
+ const logger = yield* Logger;
2536
+ return {
2537
+ notify: (msg: string) => logger.log(`Notifying: ${msg}`),
2538
+ };
2539
+ }),
2540
+ // Specify dependencies
2541
+ dependencies: [Logger.Default],
2542
+ }) {}
2543
+
2544
+ // Create a program that uses both services
2545
+ const program = Effect.gen(function* () {
2546
+ const notifier = yield* Notifier;
2547
+ yield* notifier.notify("Hello, World!");
2548
+ });
2549
+
2550
+ // Run the program with the default implementations
2551
+ Effect.runPromise(Effect.provide(program, Notifier.Default));
2552
+ ```
2553
+
2554
+ ---
2555
+
2556
+ **Anti-Pattern:**
2557
+
2558
+ Manually creating and passing service instances around. This is the "poor man's DI" and leads to tightly coupled code that is difficult to test and maintain.
2559
+
2560
+ ```typescript
2561
+ // ❌ WRONG: Manual instantiation and prop-drilling.
2562
+ class LoggerImpl {
2563
+ log(msg: string) {
2564
+ console.log(msg);
2565
+ }
2566
+ }
2567
+
2568
+ class NotifierImpl {
2569
+ constructor(private logger: LoggerImpl) {}
2570
+ notify(msg: string) {
2571
+ this.logger.log(msg);
2572
+ }
2573
+ }
2574
+
2575
+ // Dependencies must be created and passed in manually.
2576
+ const logger = new LoggerImpl();
2577
+ const notifier = new NotifierImpl(logger);
2578
+
2579
+ // This is not easily testable without creating real instances.
2580
+ notifier.notify("Hello");
2581
+ ```
2582
+
2583
+ **Rationale:**
2584
+
2585
+ Think of a `Layer<R, E, A>` as a recipe for building a service. It's a declarative blueprint that specifies:
2586
+
2587
+ - **`A` (Output)**: The service it provides (e.g., `HttpClient`).
2588
+ - **`R` (Requirements)**: The other services it needs to be built (e.g., `ConfigService`).
2589
+ - **`E` (Error)**: The errors that could occur during its construction (e.g., `ConfigError`).
2590
+
2591
+ ---
2592
+
2593
+
2594
+ In Effect, you don't create service instances directly. Instead, you define `Layer`s that describe _how_ to create them. This separation of declaration from implementation is the core of Effect's powerful dependency injection (DI) system.
2595
+
2596
+ This approach has several key benefits:
2597
+
2598
+ - **Composability:** You can combine small, focused layers into a complete application layer (`Layer.merge`, `Layer.provide`).
2599
+ - **Declarative Dependencies:** A layer's type signature explicitly documents its own dependencies, making your application's architecture clear and self-documenting.
2600
+ - **Testability:** For testing, you can easily swap a "live" layer (e.g., one that connects to a real database) with a "test" layer (one that provides mock data) without changing any of your business logic.
2601
+
2602
+ ---
2603
+
2604
+ ---
2605
+
2606
+ ### Type Classes for Equality, Ordering, and Hashing with Data.Class
2607
+
2608
+ **Rule:** Use Data.Class to define and derive type classes for your data types, supporting composable equality, ordering, and hashing.
2609
+
2610
+ **Good Example:**
2611
+
2612
+ ```typescript
2613
+ import { Data, Equal, HashSet } from "effect";
2614
+
2615
+ // Define custom data types with structural equality
2616
+ const user1 = Data.struct({ id: 1, name: "Alice" });
2617
+ const user2 = Data.struct({ id: 1, name: "Alice" });
2618
+ const user3 = Data.struct({ id: 2, name: "Bob" });
2619
+
2620
+ // Data.struct provides automatic structural equality
2621
+ console.log(Equal.equals(user1, user2)); // true (same structure)
2622
+ console.log(Equal.equals(user1, user3)); // false (different values)
2623
+
2624
+ // Use in a HashSet (works because Data.struct implements Equal)
2625
+ const set = HashSet.make(user1);
2626
+ console.log(HashSet.has(set, user2)); // true (structural equality)
2627
+
2628
+ // Create an array and use structural equality
2629
+ const users = [user1, user3];
2630
+ console.log(users.some((u) => Equal.equals(u, user2))); // true
2631
+ ```
2632
+
2633
+ **Explanation:**
2634
+
2635
+ - `Data.Class.getEqual` derives an equality type class for your data type.
2636
+ - `Data.Class.getOrder` derives an ordering type class, useful for sorting.
2637
+ - `Data.Class.getHash` derives a hash function for use in sets and maps.
2638
+ - These type classes make your types fully compatible with Effect’s collections and algorithms.
2639
+
2640
+ **Anti-Pattern:**
2641
+
2642
+ Relying on reference equality, ad-hoc comparison functions, or not providing type class instances for your custom types, which can lead to bugs and inconsistent behavior in collections.
2643
+
2644
+ **Rationale:**
2645
+
2646
+ Use `Data.Class` to derive or implement type classes for equality, ordering, and hashing for your custom data types.
2647
+ This enables composable, type-safe abstractions and allows your types to work seamlessly with Effect’s collections and algorithms.
2648
+
2649
+
2650
+ Type classes like `Equal`, `Order`, and `Hash` provide a principled way to define how your types are compared, ordered, and hashed.
2651
+ This is essential for using your types in sets, maps, and for sorting or deduplication.
2652
+
2653
+ ---
2654
+
2655
+ ### Define a Type-Safe Configuration Schema
2656
+
2657
+ **Rule:** Define a type-safe configuration schema.
2658
+
2659
+ **Good Example:**
2660
+
2661
+ ```typescript
2662
+ import { Config, Effect, ConfigProvider, Layer } from "effect";
2663
+
2664
+ const ServerConfig = Config.nested("SERVER")(
2665
+ Config.all({
2666
+ host: Config.string("HOST"),
2667
+ port: Config.number("PORT"),
2668
+ })
2669
+ );
2670
+
2671
+ // Example program that uses the config
2672
+ const program = Effect.gen(function* () {
2673
+ const config = yield* ServerConfig;
2674
+ yield* Effect.logInfo(`Server config loaded: ${JSON.stringify(config)}`);
2675
+ });
2676
+
2677
+ // Create a config provider with test values
2678
+ const TestConfig = ConfigProvider.fromMap(
2679
+ new Map([
2680
+ ["SERVER.HOST", "localhost"],
2681
+ ["SERVER.PORT", "3000"],
2682
+ ])
2683
+ );
2684
+
2685
+ // Run with test config
2686
+ Effect.runPromise(Effect.provide(program, Layer.setConfigProvider(TestConfig)));
2687
+ ```
2688
+
2689
+ **Explanation:**
2690
+ This schema ensures that both `host` and `port` are present and properly typed, and that their source is clearly defined.
2691
+
2692
+ **Anti-Pattern:**
2693
+
2694
+ Directly accessing `process.env`. This is not type-safe, scatters configuration access throughout your codebase, and can lead to parsing errors or `undefined` values.
2695
+
2696
+ **Rationale:**
2697
+
2698
+ Define all external configuration values your application needs using the schema-building functions from `Effect.Config`, such as `Config.string` and `Config.number`.
2699
+
2700
+
2701
+ This creates a single, type-safe source of truth for your configuration, eliminating runtime errors from missing or malformed environment variables and making the required configuration explicit.
2702
+
2703
+ ---
2704
+
2705
+ ### Use Chunk for High-Performance Collections
2706
+
2707
+ **Rule:** Prefer Chunk over Array for immutable collection operations within data processing pipelines for better performance.
2708
+
2709
+ **Good Example:**
2710
+
2711
+ This example shows how to create and manipulate a `Chunk`. The API is very similar to `Array`, but the underlying performance characteristics for these immutable operations are superior.
2712
+
2713
+ ```typescript
2714
+ import { Chunk, Effect } from "effect";
2715
+
2716
+ // Create a Chunk from an array
2717
+ let numbers = Chunk.fromIterable([1, 2, 3, 4, 5]);
2718
+
2719
+ // Append a new element. This is much faster than [...arr, 6] on large collections.
2720
+ numbers = Chunk.append(numbers, 6);
2721
+
2722
+ // Prepend an element.
2723
+ numbers = Chunk.prepend(numbers, 0);
2724
+
2725
+ // Take the first 3 elements
2726
+ const firstThree = Chunk.take(numbers, 3);
2727
+
2728
+ // Convert back to an array when you need to interface with other libraries
2729
+ const finalArray = Chunk.toReadonlyArray(firstThree);
2730
+
2731
+ Effect.runSync(Effect.log(finalArray)); // [0, 1, 2]
2732
+ ```
2733
+
2734
+ ---
2735
+
2736
+ **Anti-Pattern:**
2737
+
2738
+ Eagerly converting a large or potentially infinite iterable to a `Chunk` before streaming. This completely negates the memory-safety benefits of using a `Stream`.
2739
+
2740
+ ```typescript
2741
+ import { Effect, Stream, Chunk } from "effect";
2742
+
2743
+ // A generator that could produce a very large (or infinite) number of items.
2744
+ function* largeDataSource() {
2745
+ let i = 0;
2746
+ while (i < 1_000_000) {
2747
+ yield i++;
2748
+ }
2749
+ }
2750
+
2751
+ // ❌ DANGEROUS: `Chunk.fromIterable` will try to pull all 1,000,000 items
2752
+ // from the generator and load them into memory at once before the stream
2753
+ // even starts. This can lead to high memory usage or a crash.
2754
+ const programWithChunk = Stream.fromChunk(
2755
+ Chunk.fromIterable(largeDataSource())
2756
+ ).pipe(
2757
+ Stream.map((n) => n * 2),
2758
+ Stream.runDrain
2759
+ );
2760
+
2761
+ // ✅ CORRECT: `Stream.fromIterable` pulls items from the data source lazily,
2762
+ // one at a time (or in small batches), maintaining constant memory usage.
2763
+ const programWithIterable = Stream.fromIterable(largeDataSource()).pipe(
2764
+ Stream.map((n) => n * 2),
2765
+ Stream.runDrain
2766
+ );
2767
+ ```
2768
+
2769
+ **Rationale:**
2770
+
2771
+ For collections that will be heavily transformed with immutable operations (e.g., `map`, `filter`, `append`), use `Chunk<A>`. `Chunk` is Effect's implementation of a persistent and chunked vector that provides better performance than native arrays for these use cases.
2772
+
2773
+ ---
2774
+
2775
+
2776
+ JavaScript's `Array` is a mutable data structure. Every time you perform an "immutable" operation like `[...arr, newItem]` or `arr.map(...)`, you are creating a brand new array and copying all the elements from the old one. For small arrays, this is fine. For large arrays or in hot code paths, this constant allocation and copying can become a performance bottleneck.
2777
+
2778
+ `Chunk` is designed to solve this. It's an immutable data structure that uses structural sharing internally. When you append an item to a `Chunk`, it doesn't re-copy the entire collection. Instead, it creates a new `Chunk` that reuses most of the internal structure of the original, only allocating memory for the new data. This makes immutable appends and updates significantly faster.
2779
+
2780
+ ---
2781
+
2782
+ ---
2783
+
2784
+ ### Modeling Tagged Unions with Data.case
2785
+
2786
+ **Rule:** Use Data.case to define tagged unions (ADTs) for modeling domain-specific states and enabling exhaustive pattern matching.
2787
+
2788
+ **Good Example:**
2789
+
2790
+ ```typescript
2791
+ import { Data } from "effect";
2792
+
2793
+ // Define a tagged union for a simple state machine
2794
+ type State = Data.TaggedEnum<{
2795
+ Loading: {};
2796
+ Success: { data: string };
2797
+ Failure: { error: string };
2798
+ }>;
2799
+ const { Loading, Success, Failure } = Data.taggedEnum<State>();
2800
+
2801
+ // Create instances
2802
+ const state1: State = Loading();
2803
+ const state2: State = Success({ data: "Hello" });
2804
+ const state3: State = Failure({ error: "Oops" });
2805
+
2806
+ // Pattern match on the state
2807
+ function handleState(state: State): string {
2808
+ switch (state._tag) {
2809
+ case "Loading":
2810
+ return "Loading...";
2811
+ case "Success":
2812
+ return `Data: ${state.data}`;
2813
+ case "Failure":
2814
+ return `Error: ${state.error}`;
2815
+ }
2816
+ }
2817
+ ```
2818
+
2819
+ **Explanation:**
2820
+
2821
+ - `Data.case` creates tagged constructors for each state.
2822
+ - The `_tag` property enables exhaustive pattern matching.
2823
+ - Use for domain modeling, state machines, and error types.
2824
+
2825
+ **Anti-Pattern:**
2826
+
2827
+ Using plain objects or enums for domain states, which can lead to illegal states, missed cases, and less type-safe pattern matching.
2828
+
2829
+ **Rationale:**
2830
+
2831
+ Use `Data.case` to create tagged unions (algebraic data types, or ADTs) for robust, type-safe domain modeling.
2832
+ Tagged unions make it easy to represent and exhaustively handle all possible states of your domain entities.
2833
+
2834
+
2835
+ Modeling domain logic with tagged unions ensures that all cases are handled, prevents illegal states, and enables safe, exhaustive pattern matching.
2836
+ `Data.case` provides a concise, type-safe way to define and use ADTs in your application.
2837
+
2838
+ ---
2839
+
2840
+ ### Beyond the Date Type - Real World Dates, Times, and Timezones
2841
+
2842
+ **Rule:** Use the Clock service for testable time-based logic and immutable primitives for timestamps.
2843
+
2844
+ **Good Example:**
2845
+
2846
+ This example shows a function that creates a timestamped event. It depends on the `Clock` service, making it fully testable.
2847
+
2848
+ ```typescript
2849
+ import { Effect, Clock } from "effect";
2850
+ import type * as Types from "effect/Clock";
2851
+
2852
+ interface Event {
2853
+ readonly message: string;
2854
+ readonly timestamp: number; // Store as a primitive number (UTC millis)
2855
+ }
2856
+
2857
+ // This function is pure and testable because it depends on Clock
2858
+ const createEvent = (
2859
+ message: string
2860
+ ): Effect.Effect<Event, never, Types.Clock> =>
2861
+ Effect.gen(function* () {
2862
+ const timestamp = yield* Clock.currentTimeMillis;
2863
+ return { message, timestamp };
2864
+ });
2865
+
2866
+ // Create and log some events
2867
+ const program = Effect.gen(function* () {
2868
+ const loginEvent = yield* createEvent("User logged in");
2869
+ yield* Effect.log("Login event:", loginEvent);
2870
+
2871
+ const logoutEvent = yield* createEvent("User logged out");
2872
+ yield* Effect.log("Logout event:", logoutEvent);
2873
+ });
2874
+
2875
+ // Run the program
2876
+ const programWithErrorHandling = program.pipe(
2877
+ Effect.provideService(Clock.Clock, Clock.make()),
2878
+ Effect.catchAll((error) =>
2879
+ Effect.gen(function* () {
2880
+ yield* Effect.logError(`Program error: ${error}`);
2881
+ return null;
2882
+ })
2883
+ )
2884
+ );
2885
+
2886
+ Effect.runPromise(programWithErrorHandling);
2887
+ ```
2888
+
2889
+ ---
2890
+
2891
+ **Anti-Pattern:**
2892
+
2893
+ Directly using `Date.now()` or `new Date()` inside your effects. This introduces impurity and makes your logic dependent on the actual system clock, rendering it non-deterministic and difficult to test.
2894
+
2895
+ ```typescript
2896
+ import { Effect } from "effect";
2897
+
2898
+ // ❌ WRONG: This function is impure and not reliably testable.
2899
+ const createEventUnsafely = (message: string): Effect.Effect<any> =>
2900
+ Effect.sync(() => ({
2901
+ message,
2902
+ timestamp: Date.now(), // Direct call to a system API
2903
+ }));
2904
+
2905
+ // How would you test that this function assigns the correct timestamp
2906
+ // without manipulating the system clock or using complex mocks?
2907
+ ```
2908
+
2909
+ **Rationale:**
2910
+
2911
+ To handle specific points in time robustly in Effect, follow these principles:
2912
+
2913
+ 1. **Access "now" via the `Clock` service** (`Clock.currentTimeMillis`) instead of `Date.now()`.
2914
+ 2. **Store and pass timestamps** as immutable primitives: `number` for UTC milliseconds or `string` for ISO 8601 format.
2915
+ 3. **Perform calculations locally:** When you need to perform date-specific calculations (e.g., "get the day of the week"), create a `new Date(timestamp)` instance inside a pure computation, use it, and then discard it. Never hold onto mutable `Date` objects in your application state.
2916
+
2917
+ ---
2918
+
2919
+
2920
+ JavaScript's native `Date` object is a common source of bugs. It is mutable, its behavior can be inconsistent across different JavaScript environments (especially with timezones), and its reliance on the system clock makes time-dependent logic difficult to test.
2921
+
2922
+ Effect's approach solves these problems:
2923
+
2924
+ - The **`Clock` service** abstracts away the concept of "now." In production, the `Live` clock uses the system time. In tests, you can provide a `TestClock` that gives you complete, deterministic control over the passage of time.
2925
+ - Using **primitive `number` or `string`** for timestamps ensures immutability and makes your data easy to serialize, store, and transfer.
2926
+
2927
+ This makes your time-based logic pure, predictable, and easy to test.
2928
+
2929
+ ---
2930
+
2931
+ ---
2932
+
2933
+ ### Mapping and Chaining over Collections with forEach and all
2934
+
2935
+ **Rule:** Use forEach and all to process collections of values with effectful functions, collecting results in a type-safe and composable way.
2936
+
2937
+ **Good Example:**
2938
+
2939
+ ```typescript
2940
+ import { Effect, Either, Option, Stream } from "effect";
2941
+
2942
+ // Effect: Apply an effectful function to each item in an array
2943
+ const numbers = [1, 2, 3];
2944
+ const effect = Effect.forEach(numbers, (n) => Effect.succeed(n * 2));
2945
+ // Effect<number[]>
2946
+
2947
+ // Effect: Run multiple effects in parallel and collect results
2948
+ const effects = [Effect.succeed(1), Effect.succeed(2)];
2949
+ const allEffect = Effect.all(effects, { concurrency: "unbounded" }); // Effect<[1, 2]>
2950
+
2951
+ // Option: Map over a collection of options and collect only the Some values
2952
+ const options = [Option.some(1), Option.none(), Option.some(3)];
2953
+ const filtered = options.filter(Option.isSome).map((o) => o.value); // [1, 3]
2954
+
2955
+ // Either: Collect all Right values from a collection of Eithers
2956
+ const eithers = [Either.right(1), Either.left("fail"), Either.right(3)];
2957
+ const rights = eithers.filter(Either.isRight); // [Either.Right(1), Either.Right(3)]
2958
+
2959
+ // Stream: Map and flatten a stream of arrays
2960
+ const stream = Stream.fromIterable([
2961
+ [1, 2],
2962
+ [3, 4],
2963
+ ]).pipe(Stream.flatMap((arr) => Stream.fromIterable(arr))); // Stream<number>
2964
+ ```
2965
+
2966
+ **Explanation:**
2967
+ `forEach` and `all` let you process collections in a way that is composable, type-safe, and often parallel.
2968
+ They handle errors and context automatically, and can be used for batch jobs, parallel requests, or data transformations.
2969
+
2970
+ **Anti-Pattern:**
2971
+
2972
+ Using manual loops (`for`, `forEach`, etc.) with side effects, or collecting results imperatively, which breaks composability and loses error/context handling.
2973
+
2974
+ **Rationale:**
2975
+
2976
+ Use the `forEach` and `all` combinators to apply an effectful function to every item in a collection and combine the results.
2977
+ This enables you to process lists, arrays, or other collections in a type-safe, composable, and often parallel way.
2978
+
2979
+
2980
+ Batch and parallel processing are common in real-world applications.
2981
+ These combinators let you express "do this for every item" declaratively, without manual loops or imperative control flow, and they preserve error handling and context propagation.
2982
+
2983
+ ---
2984
+
2985
+ ### Provide Configuration to Your App via a Layer
2986
+
2987
+ **Rule:** Provide configuration to your app via a Layer.
2988
+
2989
+ **Good Example:**
2990
+
2991
+ ```typescript
2992
+ import { Effect, Layer } from "effect";
2993
+
2994
+ class ServerConfig extends Effect.Service<ServerConfig>()("ServerConfig", {
2995
+ sync: () => ({
2996
+ port: process.env.PORT ? parseInt(process.env.PORT) : 8080,
2997
+ }),
2998
+ }) {}
2999
+
3000
+ const program = Effect.gen(function* () {
3001
+ const config = yield* ServerConfig;
3002
+ yield* Effect.log(`Starting application on port ${config.port}...`);
3003
+ });
3004
+
3005
+ const programWithErrorHandling = Effect.provide(
3006
+ program,
3007
+ ServerConfig.Default
3008
+ ).pipe(
3009
+ Effect.catchAll((error) =>
3010
+ Effect.gen(function* () {
3011
+ yield* Effect.logError(`Program error: ${error}`);
3012
+ return null;
3013
+ })
3014
+ )
3015
+ );
3016
+
3017
+ Effect.runPromise(programWithErrorHandling);
3018
+ ```
3019
+
3020
+ **Explanation:**
3021
+ This approach makes configuration available contextually, supporting better testing and modularity.
3022
+
3023
+ **Anti-Pattern:**
3024
+
3025
+ Manually reading environment variables deep inside business logic. This tightly couples that logic to the external environment, making it difficult to test and reuse.
3026
+
3027
+ **Rationale:**
3028
+
3029
+ Transform your configuration schema into a `Layer` using `Config.layer()` and provide it to your main application `Effect`.
3030
+
3031
+
3032
+ Integrating configuration as a `Layer` plugs it directly into Effect's dependency injection system. This makes your configuration available anywhere in the program and dramatically simplifies testing by allowing you to substitute mock configuration.
3033
+
3034
+ ---
3035
+
3036
+ ### Work with Dates and Times using DateTime
3037
+
3038
+ **Rule:** Use DateTime to represent and manipulate dates and times in a type-safe, immutable, and time-zone-aware way.
3039
+
3040
+ **Good Example:**
3041
+
3042
+ ```typescript
3043
+ import { DateTime } from "effect";
3044
+
3045
+ // Create a DateTime for the current instant (returns an Effect)
3046
+ import { Effect } from "effect";
3047
+
3048
+ const program = Effect.gen(function* () {
3049
+ const now = yield* DateTime.now; // DateTime.Utc
3050
+
3051
+ // Parse from ISO string
3052
+ const parsed = DateTime.unsafeMakeZoned("2024-07-19T12:34:56Z"); // DateTime.Zoned
3053
+
3054
+ // Add or subtract durations
3055
+ const inOneHour = DateTime.add(now, { hours: 1 });
3056
+ const oneHourAgo = DateTime.subtract(now, { hours: 1 });
3057
+
3058
+ // Format as ISO string
3059
+ const iso = DateTime.formatIso(now); // e.g., "2024-07-19T23:33:19.000Z"
3060
+
3061
+ // Compare DateTimes
3062
+ const isBefore = DateTime.lessThan(oneHourAgo, now); // true
3063
+
3064
+ return { now, inOneHour, oneHourAgo, iso, isBefore };
3065
+ });
3066
+ ```
3067
+
3068
+ **Explanation:**
3069
+
3070
+ - `DateTime` is immutable and time-zone-aware.
3071
+ - Supports parsing, formatting, arithmetic, and comparison.
3072
+ - Use for all date/time logic to avoid bugs with native `Date`.
3073
+
3074
+ **Anti-Pattern:**
3075
+
3076
+ Using JavaScript's mutable `Date` for time calculations, or ignoring time zones, which can lead to subtle and hard-to-debug errors.
3077
+
3078
+ **Rationale:**
3079
+
3080
+ Use the `DateTime` data type to represent and manipulate dates and times in a type-safe, immutable, and time-zone-aware way.
3081
+ This enables safe, precise, and reliable time calculations in your applications.
3082
+
3083
+
3084
+ JavaScript's native `Date` is mutable, not time-zone-aware, and can be error-prone.
3085
+ `DateTime` provides an immutable, functional alternative with explicit time zone handling and robust APIs for time arithmetic.
3086
+
3087
+ ---
3088
+
3089
+ ### Manage Shared State Safely with Ref
3090
+
3091
+ **Rule:** Use Ref to safely manage shared, mutable state in concurrent and effectful programs.
3092
+
3093
+ **Good Example:**
3094
+
3095
+ ```typescript
3096
+ import { Effect, Ref } from "effect";
3097
+
3098
+ // Create a Ref with an initial value
3099
+ const makeCounter = Ref.make(0);
3100
+
3101
+ // Increment the counter atomically
3102
+ const increment = makeCounter.pipe(
3103
+ Effect.flatMap((counter) => Ref.update(counter, (n) => n + 1))
3104
+ );
3105
+
3106
+ // Read the current value
3107
+ const getValue = makeCounter.pipe(
3108
+ Effect.flatMap((counter) => Ref.get(counter))
3109
+ );
3110
+
3111
+ // Use Ref in a workflow
3112
+ const program = Effect.gen(function* () {
3113
+ const counter = yield* Ref.make(0);
3114
+ yield* Ref.update(counter, (n) => n + 1);
3115
+ const value = yield* Ref.get(counter);
3116
+ yield* Effect.log(`Counter value: ${value}`);
3117
+ });
3118
+ ```
3119
+
3120
+ **Explanation:**
3121
+
3122
+ - `Ref` is an atomic, mutable reference for effectful and concurrent code.
3123
+ - All operations are safe, composable, and free of race conditions.
3124
+ - Use `Ref` for counters, caches, or any shared mutable state.
3125
+
3126
+ **Anti-Pattern:**
3127
+
3128
+ Using plain variables or objects for shared state in concurrent or async code, which can lead to race conditions, bugs, and unpredictable behavior.
3129
+
3130
+ **Rationale:**
3131
+
3132
+ Use the `Ref<A>` data type to model shared, mutable state in a concurrent environment.
3133
+ `Ref` provides atomic, thread-safe operations for reading and updating state in effectful programs.
3134
+
3135
+
3136
+ Managing shared state with plain variables or objects is unsafe in concurrent or asynchronous code.
3137
+ `Ref` ensures all updates are atomic and free of race conditions, making your code robust and predictable.
3138
+
3139
+ ---
3140
+
3141
+
3142
+ ## 🟠 Advanced Patterns
3143
+
3144
+ ### Handle Unexpected Errors by Inspecting the Cause
3145
+
3146
+ **Rule:** Use Cause to inspect, analyze, and handle all possible failure modes of an Effect, including expected errors, defects, and interruptions.
3147
+
3148
+ **Good Example:**
3149
+
3150
+ ```typescript
3151
+ import { Cause, Effect } from "effect";
3152
+
3153
+ // An Effect that may fail with an error or defect
3154
+ const program = Effect.try({
3155
+ try: () => {
3156
+ throw new Error("Unexpected failure!");
3157
+ },
3158
+ catch: (err) => err,
3159
+ });
3160
+
3161
+ // Catch all causes and inspect them
3162
+ const handled = program.pipe(
3163
+ Effect.catchAllCause((cause) =>
3164
+ Effect.sync(() => {
3165
+ if (Cause.isDie(cause)) {
3166
+ console.error("Defect (die):", Cause.pretty(cause));
3167
+ } else if (Cause.isFailure(cause)) {
3168
+ console.error("Expected error:", Cause.pretty(cause));
3169
+ } else if (Cause.isInterrupted(cause)) {
3170
+ console.error("Interrupted:", Cause.pretty(cause));
3171
+ }
3172
+ // Handle or rethrow as needed
3173
+ })
3174
+ )
3175
+ );
3176
+ ```
3177
+
3178
+ **Explanation:**
3179
+
3180
+ - `Cause` distinguishes between expected errors (`fail`), defects (`die`), and interruptions.
3181
+ - Use `Cause.pretty` for human-readable error traces.
3182
+ - Enables advanced error handling and debugging.
3183
+
3184
+ **Anti-Pattern:**
3185
+
3186
+ Catching only expected errors and ignoring defects or interruptions, which can lead to silent failures, missed bugs, and harder debugging.
3187
+
3188
+ **Rationale:**
3189
+
3190
+ Use the `Cause<E>` data type to get rich, structured information about errors and failures in your Effects.
3191
+ `Cause` captures not just expected errors, but also defects (unhandled exceptions), interruptions, and error traces.
3192
+
3193
+
3194
+ Traditional error handling often loses information about _why_ a failure occurred.
3195
+ `Cause` preserves the full error context, enabling advanced debugging, error reporting, and robust recovery strategies.
3196
+
3197
+ ---
3198
+
3199
+