@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,1385 @@
1
+ ---
2
+ name: effect-patterns-domain-modeling
3
+ description: Effect-TS patterns for Domain Modeling. Use when working with domain modeling in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Domain Modeling
6
+ This skill provides 15 curated Effect-TS patterns for domain modeling.
7
+ Use this skill when working on tasks related to:
8
+ - domain modeling
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟢 Beginner Patterns
15
+
16
+ ### Create Type-Safe Errors
17
+
18
+ **Rule:** Use Data.TaggedError to create typed, distinguishable errors for your domain.
19
+
20
+ **Good Example:**
21
+
22
+ ```typescript
23
+ import { Effect, Data } from "effect"
24
+
25
+ // ============================================
26
+ // 1. Define tagged errors for your domain
27
+ // ============================================
28
+
29
+ class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
30
+ readonly userId: string
31
+ }> {}
32
+
33
+ class InvalidEmailError extends Data.TaggedError("InvalidEmailError")<{
34
+ readonly email: string
35
+ readonly reason: string
36
+ }> {}
37
+
38
+ class DuplicateUserError extends Data.TaggedError("DuplicateUserError")<{
39
+ readonly email: string
40
+ }> {}
41
+
42
+ // ============================================
43
+ // 2. Use in Effect functions
44
+ // ============================================
45
+
46
+ interface User {
47
+ id: string
48
+ email: string
49
+ name: string
50
+ }
51
+
52
+ const validateEmail = (email: string): Effect.Effect<string, InvalidEmailError> => {
53
+ if (!email.includes("@")) {
54
+ return Effect.fail(new InvalidEmailError({
55
+ email,
56
+ reason: "Missing @ symbol"
57
+ }))
58
+ }
59
+ return Effect.succeed(email)
60
+ }
61
+
62
+ const findUser = (id: string): Effect.Effect<User, UserNotFoundError> => {
63
+ // Simulate database lookup
64
+ if (id === "123") {
65
+ return Effect.succeed({ id, email: "alice@example.com", name: "Alice" })
66
+ }
67
+ return Effect.fail(new UserNotFoundError({ userId: id }))
68
+ }
69
+
70
+ const createUser = (
71
+ email: string,
72
+ name: string
73
+ ): Effect.Effect<User, InvalidEmailError | DuplicateUserError> =>
74
+ Effect.gen(function* () {
75
+ const validEmail = yield* validateEmail(email)
76
+
77
+ // Simulate duplicate check
78
+ if (validEmail === "taken@example.com") {
79
+ return yield* Effect.fail(new DuplicateUserError({ email: validEmail }))
80
+ }
81
+
82
+ return {
83
+ id: crypto.randomUUID(),
84
+ email: validEmail,
85
+ name,
86
+ }
87
+ })
88
+
89
+ // ============================================
90
+ // 3. Handle errors by tag
91
+ // ============================================
92
+
93
+ const program = createUser("alice@example.com", "Alice").pipe(
94
+ Effect.catchTag("InvalidEmailError", (error) =>
95
+ Effect.succeed({
96
+ id: "fallback",
97
+ email: "default@example.com",
98
+ name: `${error.email} was invalid: ${error.reason}`,
99
+ })
100
+ ),
101
+ Effect.catchTag("DuplicateUserError", (error) =>
102
+ Effect.fail(new Error(`Email ${error.email} already registered`))
103
+ )
104
+ )
105
+
106
+ // ============================================
107
+ // 4. Match on all errors
108
+ // ============================================
109
+
110
+ const handleAllErrors = createUser("bad-email", "Bob").pipe(
111
+ Effect.catchTags({
112
+ InvalidEmailError: (e) => Effect.succeed(`Invalid: ${e.reason}`),
113
+ DuplicateUserError: (e) => Effect.succeed(`Duplicate: ${e.email}`),
114
+ })
115
+ )
116
+
117
+ // ============================================
118
+ // 5. Run and see results
119
+ // ============================================
120
+
121
+ Effect.runPromise(program)
122
+ .then((user) => console.log("Created:", user))
123
+ .catch((error) => console.error("Failed:", error))
124
+ ```
125
+
126
+ **Rationale:**
127
+
128
+ Create domain-specific errors using `Data.TaggedError`. Each error type gets a unique `_tag` for pattern matching.
129
+
130
+ ---
131
+
132
+
133
+ Plain `Error` or string messages cause problems:
134
+
135
+ 1. **No type safety** - Can't know what errors a function might throw
136
+ 2. **Hard to handle** - Matching on error messages is fragile
137
+ 3. **Poor documentation** - Errors aren't part of the function signature
138
+
139
+ Tagged errors solve this by making errors typed and distinguishable.
140
+
141
+ ---
142
+
143
+ ---
144
+
145
+ ### Handle Missing Values with Option
146
+
147
+ **Rule:** Use Option instead of null/undefined to make missing values explicit and type-safe.
148
+
149
+ **Good Example:**
150
+
151
+ ```typescript
152
+ import { Option, Effect } from "effect"
153
+
154
+ // ============================================
155
+ // 1. Creating Options
156
+ // ============================================
157
+
158
+ // Some - a value is present
159
+ const hasValue = Option.some(42)
160
+
161
+ // None - no value
162
+ const noValue = Option.none<number>()
163
+
164
+ // From nullable - null/undefined becomes None
165
+ const fromNull = Option.fromNullable(null) // None
166
+ const fromValue = Option.fromNullable("hello") // Some("hello")
167
+
168
+ // ============================================
169
+ // 2. Checking and extracting values
170
+ // ============================================
171
+
172
+ const maybeUser = Option.some({ name: "Alice", age: 30 })
173
+
174
+ // Check if value exists
175
+ if (Option.isSome(maybeUser)) {
176
+ console.log(`User: ${maybeUser.value.name}`)
177
+ }
178
+
179
+ // Get with default
180
+ const name = Option.getOrElse(
181
+ Option.map(maybeUser, u => u.name),
182
+ () => "Anonymous"
183
+ )
184
+
185
+ // ============================================
186
+ // 3. Transforming Options
187
+ // ============================================
188
+
189
+ const maybeNumber = Option.some(5)
190
+
191
+ // Map - transform the value if present
192
+ const doubled = Option.map(maybeNumber, n => n * 2) // Some(10)
193
+
194
+ // FlatMap - chain operations that return Option
195
+ const safeDivide = (a: number, b: number): Option.Option<number> =>
196
+ b === 0 ? Option.none() : Option.some(a / b)
197
+
198
+ const result = Option.flatMap(maybeNumber, n => safeDivide(10, n)) // Some(2)
199
+
200
+ // ============================================
201
+ // 4. Domain modeling example
202
+ // ============================================
203
+
204
+ interface User {
205
+ readonly id: string
206
+ readonly name: string
207
+ readonly email: Option.Option<string> // Email is optional
208
+ readonly phone: Option.Option<string> // Phone is optional
209
+ }
210
+
211
+ const createUser = (name: string): User => ({
212
+ id: crypto.randomUUID(),
213
+ name,
214
+ email: Option.none(),
215
+ phone: Option.none(),
216
+ })
217
+
218
+ const addEmail = (user: User, email: string): User => ({
219
+ ...user,
220
+ email: Option.some(email),
221
+ })
222
+
223
+ const getContactInfo = (user: User): string => {
224
+ const email = Option.getOrElse(user.email, () => "no email")
225
+ const phone = Option.getOrElse(user.phone, () => "no phone")
226
+ return `${user.name}: ${email}, ${phone}`
227
+ }
228
+
229
+ // ============================================
230
+ // 5. Use in Effects
231
+ // ============================================
232
+
233
+ const findUser = (id: string): Effect.Effect<Option.Option<User>> =>
234
+ Effect.succeed(
235
+ id === "123"
236
+ ? Option.some({ id, name: "Alice", email: Option.none(), phone: Option.none() })
237
+ : Option.none()
238
+ )
239
+
240
+ const program = Effect.gen(function* () {
241
+ const maybeUser = yield* findUser("123")
242
+
243
+ if (Option.isSome(maybeUser)) {
244
+ yield* Effect.log(`Found: ${maybeUser.value.name}`)
245
+ } else {
246
+ yield* Effect.log("User not found")
247
+ }
248
+ })
249
+
250
+ Effect.runPromise(program)
251
+ ```
252
+
253
+ **Rationale:**
254
+
255
+ Use `Option<A>` to represent values that might be absent. This makes "might not exist" explicit in your types, forcing you to handle both cases.
256
+
257
+ ---
258
+
259
+
260
+ `null` and `undefined` cause bugs because:
261
+
262
+ 1. **Silent failures** - Accessing `.property` on null crashes at runtime
263
+ 2. **Unclear intent** - Is null "not found" or "error"?
264
+ 3. **Forgotten checks** - Easy to forget `if (x !== null)`
265
+
266
+ Option fixes this by making absence explicit and type-checked.
267
+
268
+ ---
269
+
270
+ ---
271
+
272
+ ### Your First Domain Model
273
+
274
+ **Rule:** Start domain modeling by defining clear interfaces for your business entities.
275
+
276
+ **Good Example:**
277
+
278
+ ```typescript
279
+ import { Effect } from "effect"
280
+
281
+ // ============================================
282
+ // 1. Define domain entities as interfaces
283
+ // ============================================
284
+
285
+ interface User {
286
+ readonly id: string
287
+ readonly email: string
288
+ readonly name: string
289
+ readonly createdAt: Date
290
+ }
291
+
292
+ interface Product {
293
+ readonly sku: string
294
+ readonly name: string
295
+ readonly price: number
296
+ readonly inStock: boolean
297
+ }
298
+
299
+ interface Order {
300
+ readonly id: string
301
+ readonly userId: string
302
+ readonly items: ReadonlyArray<OrderItem>
303
+ readonly total: number
304
+ readonly status: OrderStatus
305
+ }
306
+
307
+ interface OrderItem {
308
+ readonly productSku: string
309
+ readonly quantity: number
310
+ readonly unitPrice: number
311
+ }
312
+
313
+ type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered"
314
+
315
+ // ============================================
316
+ // 2. Create domain functions
317
+ // ============================================
318
+
319
+ const createUser = (email: string, name: string): User => ({
320
+ id: crypto.randomUUID(),
321
+ email,
322
+ name,
323
+ createdAt: new Date(),
324
+ })
325
+
326
+ const calculateOrderTotal = (items: ReadonlyArray<OrderItem>): number =>
327
+ items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0)
328
+
329
+ // ============================================
330
+ // 3. Use in Effect programs
331
+ // ============================================
332
+
333
+ const program = Effect.gen(function* () {
334
+ const user = createUser("alice@example.com", "Alice")
335
+ yield* Effect.log(`Created user: ${user.name}`)
336
+
337
+ const items: OrderItem[] = [
338
+ { productSku: "WIDGET-001", quantity: 2, unitPrice: 29.99 },
339
+ { productSku: "GADGET-002", quantity: 1, unitPrice: 49.99 },
340
+ ]
341
+
342
+ const order: Order = {
343
+ id: crypto.randomUUID(),
344
+ userId: user.id,
345
+ items,
346
+ total: calculateOrderTotal(items),
347
+ status: "pending",
348
+ }
349
+
350
+ yield* Effect.log(`Order total: $${order.total.toFixed(2)}`)
351
+ return order
352
+ })
353
+
354
+ Effect.runPromise(program)
355
+ ```
356
+
357
+ **Rationale:**
358
+
359
+ Start by defining TypeScript interfaces that represent your business entities. Use descriptive names that match your domain language.
360
+
361
+ ---
362
+
363
+
364
+ Good domain modeling:
365
+
366
+ 1. **Clarifies intent** - Types document what data means
367
+ 2. **Prevents errors** - Compiler catches wrong data usage
368
+ 3. **Enables tooling** - IDE autocompletion and refactoring
369
+ 4. **Communicates** - Code becomes documentation
370
+
371
+ ---
372
+
373
+ ---
374
+
375
+
376
+ ## 🟡 Intermediate Patterns
377
+
378
+ ### Model Optional Values Safely with Option
379
+
380
+ **Rule:** Use Option<A> to explicitly model values that may be absent, avoiding null or undefined.
381
+
382
+ **Good Example:**
383
+
384
+ A function that looks for a user in a database is a classic use case. It might find a user, or it might not. Returning an `Option<User>` makes this contract explicit and safe.
385
+
386
+ ```typescript
387
+ import { Effect, Option } from "effect";
388
+
389
+ interface User {
390
+ id: number;
391
+ name: string;
392
+ }
393
+
394
+ const users: User[] = [
395
+ { id: 1, name: "Paul" },
396
+ { id: 2, name: "Alex" },
397
+ ];
398
+
399
+ // This function safely returns an Option, not a User or null.
400
+ const findUserById = (id: number): Option.Option<User> => {
401
+ const user = users.find((u) => u.id === id);
402
+ return Option.fromNullable(user); // A useful helper for existing APIs
403
+ };
404
+
405
+ // The caller MUST handle both cases.
406
+ const greeting = (id: number): string =>
407
+ findUserById(id).pipe(
408
+ Option.match({
409
+ onNone: () => "User not found.",
410
+ onSome: (user) => `Welcome, ${user.name}!​`,
411
+ })
412
+ );
413
+
414
+ const program = Effect.gen(function* () {
415
+ yield* Effect.log(greeting(1)); // "Welcome, Paul!"
416
+ yield* Effect.log(greeting(3)); // "User not found."
417
+ });
418
+
419
+ Effect.runPromise(program);
420
+ ```
421
+
422
+ **Anti-Pattern:**
423
+
424
+ The anti-pattern is returning a nullable type (e.g., User | null or User | undefined). This relies on the discipline of every single caller to perform a null check. Forgetting even one check can introduce a runtime error.
425
+
426
+ ```typescript
427
+ interface User {
428
+ id: number;
429
+ name: string;
430
+ }
431
+ const users: User[] = [{ id: 1, name: "Paul" }];
432
+
433
+ // ❌ WRONG: This function's return type is less safe.
434
+ const findUserUnsafely = (id: number): User | undefined => {
435
+ return users.find((u) => u.id === id);
436
+ };
437
+
438
+ const user = findUserUnsafely(3);
439
+
440
+ // This will throw "TypeError: Cannot read properties of undefined (reading 'name')"
441
+ // because the caller forgot to check if the user exists.
442
+ console.log(`User's name is ${user.name}`);
443
+ ```
444
+
445
+ **Rationale:**
446
+
447
+ Represent values that may be absent with `Option<A>`. Use `Option.some(value)` to represent a present value and `Option.none()` for an absent one. This creates a container that forces you to handle both possibilities.
448
+
449
+ ---
450
+
451
+
452
+ Functions that can return a value or `null`/`undefined` are a primary source of runtime errors in TypeScript (`Cannot read properties of null`).
453
+
454
+ The `Option` type solves this by making the possibility of an absent value explicit in the type system. A function that returns `Option<User>` cannot be mistaken for a function that returns `User`. The compiler forces you to handle the `None` case before you can access the value inside a `Some`, eliminating an entire class of bugs.
455
+
456
+ ---
457
+
458
+ ---
459
+
460
+ ### Use Effect.gen for Business Logic
461
+
462
+ **Rule:** Use Effect.gen for business logic.
463
+
464
+ **Good Example:**
465
+
466
+ ```typescript
467
+ import { Effect } from "effect";
468
+
469
+ // Concrete implementations for demonstration
470
+ const validateUser = (
471
+ data: any
472
+ ): Effect.Effect<{ email: string; password: string }, Error, never> =>
473
+ Effect.gen(function* () {
474
+ yield* Effect.logInfo(`Validating user data: ${JSON.stringify(data)}`);
475
+
476
+ if (!data.email || !data.password) {
477
+ return yield* Effect.fail(new Error("Email and password are required"));
478
+ }
479
+
480
+ if (data.password.length < 6) {
481
+ return yield* Effect.fail(
482
+ new Error("Password must be at least 6 characters")
483
+ );
484
+ }
485
+
486
+ yield* Effect.logInfo("✅ User data validated successfully");
487
+ return { email: data.email, password: data.password };
488
+ });
489
+
490
+ const hashPassword = (pw: string): Effect.Effect<string, never, never> =>
491
+ Effect.gen(function* () {
492
+ yield* Effect.logInfo("Hashing password...");
493
+ // Simulate password hashing
494
+ const timestamp = yield* Effect.sync(() => Date.now());
495
+ const hashed = `hashed_${pw}_${timestamp}`;
496
+ yield* Effect.logInfo("✅ Password hashed successfully");
497
+ return hashed;
498
+ });
499
+
500
+ const dbCreateUser = (data: {
501
+ email: string;
502
+ password: string;
503
+ }): Effect.Effect<{ id: number; email: string }, never, never> =>
504
+ Effect.gen(function* () {
505
+ yield* Effect.logInfo(`Creating user in database: ${data.email}`);
506
+ // Simulate database operation
507
+ const user = { id: Math.floor(Math.random() * 1000), email: data.email };
508
+ yield* Effect.logInfo(`✅ User created with ID: ${user.id}`);
509
+ return user;
510
+ });
511
+
512
+ const createUser = (
513
+ userData: any
514
+ ): Effect.Effect<{ id: number; email: string }, Error, never> =>
515
+ Effect.gen(function* () {
516
+ const validated = yield* validateUser(userData);
517
+ const hashed = yield* hashPassword(validated.password);
518
+ return yield* dbCreateUser({ ...validated, password: hashed });
519
+ });
520
+
521
+ // Demonstrate using Effect.gen for business logic
522
+ const program = Effect.gen(function* () {
523
+ yield* Effect.logInfo("=== Using Effect.gen for Business Logic Demo ===");
524
+
525
+ // Example 1: Successful user creation
526
+ yield* Effect.logInfo("\n1. Creating a valid user:");
527
+ const validUser = yield* createUser({
528
+ email: "paul@example.com",
529
+ password: "securepassword123",
530
+ }).pipe(
531
+ Effect.catchAll((error) =>
532
+ Effect.gen(function* () {
533
+ yield* Effect.logError(`Failed to create user: ${error.message}`);
534
+ return { id: -1, email: "error" };
535
+ })
536
+ )
537
+ );
538
+ yield* Effect.logInfo(`Created user: ${JSON.stringify(validUser)}`);
539
+
540
+ // Example 2: Invalid user data
541
+ yield* Effect.logInfo("\n2. Attempting to create user with invalid data:");
542
+ const invalidUser = yield* createUser({
543
+ email: "invalid@example.com",
544
+ password: "123", // Too short
545
+ }).pipe(
546
+ Effect.catchAll((error) =>
547
+ Effect.gen(function* () {
548
+ yield* Effect.logError(`Failed to create user: ${error.message}`);
549
+ return { id: -1, email: "error" };
550
+ })
551
+ )
552
+ );
553
+ yield* Effect.logInfo(`Result: ${JSON.stringify(invalidUser)}`);
554
+
555
+ yield* Effect.logInfo("\n✅ Business logic demonstration completed!");
556
+ });
557
+
558
+ Effect.runPromise(program);
559
+ ```
560
+
561
+ **Explanation:**
562
+ `Effect.gen` allows you to express business logic in a clear, sequential style,
563
+ improving maintainability.
564
+
565
+ **Anti-Pattern:**
566
+
567
+ Using long chains of `.andThen` or `.flatMap` for multi-step business logic.
568
+ This is harder to read and pass state between steps.
569
+
570
+ **Rationale:**
571
+
572
+ Use `Effect.gen` to write your core business logic, especially when it involves
573
+ multiple sequential steps or conditional branching.
574
+
575
+
576
+ Generators provide a syntax that closely resembles standard synchronous code
577
+ (`async/await`), making complex workflows significantly easier to read, write,
578
+ and debug.
579
+
580
+ ---
581
+
582
+ ### Transform Data During Validation with Schema
583
+
584
+ **Rule:** Use Schema.transform to safely convert data types during the validation and parsing process.
585
+
586
+ **Good Example:**
587
+
588
+ This schema parses a string but produces a `Date` object, making the final data structure much more useful.
589
+
590
+ ```typescript
591
+ import { Schema, Effect } from "effect";
592
+
593
+ // Define types for better type safety
594
+ type RawEvent = {
595
+ name: string;
596
+ timestamp: string;
597
+ };
598
+
599
+ type ParsedEvent = {
600
+ name: string;
601
+ timestamp: Date;
602
+ };
603
+
604
+ // Define the schema for our event
605
+ const ApiEventSchema = Schema.Struct({
606
+ name: Schema.String,
607
+ timestamp: Schema.String,
608
+ });
609
+
610
+ // Example input
611
+ const rawInput: RawEvent = {
612
+ name: "User Login",
613
+ timestamp: "2025-06-22T20:08:42.000Z",
614
+ };
615
+
616
+ // Parse and transform
617
+ const program = Effect.gen(function* () {
618
+ const parsed = yield* Schema.decode(ApiEventSchema)(rawInput);
619
+ return {
620
+ name: parsed.name,
621
+ timestamp: new Date(parsed.timestamp),
622
+ } as ParsedEvent;
623
+ });
624
+
625
+ const programWithLogging = Effect.gen(function* () {
626
+ try {
627
+ const event = yield* program;
628
+ yield* Effect.log(`Event year: ${event.timestamp.getFullYear()}`);
629
+ yield* Effect.log(`Full event: ${JSON.stringify(event, null, 2)}`);
630
+ return event;
631
+ } catch (error) {
632
+ yield* Effect.logError(`Failed to parse event: ${error}`);
633
+ throw error;
634
+ }
635
+ }).pipe(
636
+ Effect.catchAll((error) =>
637
+ Effect.gen(function* () {
638
+ yield* Effect.logError(`Program error: ${error}`);
639
+ return null;
640
+ })
641
+ )
642
+ );
643
+
644
+ Effect.runPromise(programWithLogging);
645
+ ```
646
+
647
+
648
+ `transformOrFail` is perfect for creating branded types, as the validation can fail.
649
+
650
+ ```typescript
651
+ import { Schema, Effect, Brand, Either } from "effect";
652
+
653
+ type Email = string & Brand.Brand<"Email">;
654
+ const Email = Schema.string.pipe(
655
+ Schema.transformOrFail(
656
+ Schema.brand<Email>("Email"),
657
+ (s, _, ast) =>
658
+ s.includes("@")
659
+ ? Either.right(s as Email)
660
+ : Either.left(Schema.ParseError.create(ast, "Invalid email format")),
661
+ (email) => Either.right(email)
662
+ )
663
+ );
664
+
665
+ const result = Schema.decode(Email)("paul@example.com"); // Succeeds
666
+ const errorResult = Schema.decode(Email)("invalid-email"); // Fails
667
+ ```
668
+
669
+ ---
670
+
671
+ **Anti-Pattern:**
672
+
673
+ Performing validation and transformation in two separate steps. This is more verbose, requires creating intermediate types, and separates the validation logic from the transformation logic.
674
+
675
+ ```typescript
676
+ import { Schema, Effect } from "effect";
677
+
678
+ // ❌ WRONG: Requires an intermediate "Raw" type.
679
+ const RawApiEventSchema = Schema.Struct({
680
+ name: Schema.String,
681
+ timestamp: Schema.String,
682
+ });
683
+
684
+ const rawInput = { name: "User Login", timestamp: "2025-06-22T20:08:42.000Z" };
685
+
686
+ // The logic is now split into two distinct, less cohesive steps.
687
+ const program = Schema.decode(RawApiEventSchema)(rawInput).pipe(
688
+ Effect.map((rawEvent) => ({
689
+ ...rawEvent,
690
+ timestamp: new Date(rawEvent.timestamp), // Manual transformation after parsing.
691
+ }))
692
+ );
693
+ ```
694
+
695
+ **Rationale:**
696
+
697
+ To convert data from one type to another as part of the validation process, use `Schema.transform`. This allows you to define a schema that parses an input type (e.g., `string`) and outputs a different, richer domain type (e.g., `Date`).
698
+
699
+ ---
700
+
701
+
702
+ Often, the data you receive from external sources (like an API) isn't in the ideal format for your application's domain model. For example, dates are sent as ISO strings, but you want to work with `Date` objects.
703
+
704
+ `Schema.transform` integrates this conversion directly into the parsing step. It takes two functions: one to `decode` the input type into the domain type, and one to `encode` it back. This makes your schema the single source of truth for both the shape and the type transformation of your data.
705
+
706
+ For transformations that can fail (like creating a branded type), you can use `Schema.transformOrFail`, which allows the decoding step to return an `Either`.
707
+
708
+ ---
709
+
710
+ ---
711
+
712
+ ### Define Type-Safe Errors with Data.TaggedError
713
+
714
+ **Rule:** Define type-safe errors with Data.TaggedError.
715
+
716
+ **Good Example:**
717
+
718
+ ```typescript
719
+ import { Data, Effect } from "effect";
720
+
721
+ // Define our tagged error type
722
+ class DatabaseError extends Data.TaggedError("DatabaseError")<{
723
+ readonly cause: unknown;
724
+ }> {}
725
+
726
+ // Function that simulates a database error
727
+ const findUser = (
728
+ id: number
729
+ ): Effect.Effect<{ id: number; name: string }, DatabaseError> =>
730
+ Effect.gen(function* () {
731
+ if (id < 0) {
732
+ return yield* Effect.fail(new DatabaseError({ cause: "Invalid ID" }));
733
+ }
734
+ return { id, name: `User ${id}` };
735
+ });
736
+
737
+ // Create a program that demonstrates error handling
738
+ const program = Effect.gen(function* () {
739
+ // Try to find a valid user
740
+ yield* Effect.logInfo("Looking up user 1...");
741
+ yield* Effect.gen(function* () {
742
+ const user = yield* findUser(1);
743
+ yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
744
+ }).pipe(
745
+ Effect.catchAll((error) =>
746
+ Effect.logInfo(`Error finding user: ${error._tag} - ${error.cause}`)
747
+ )
748
+ );
749
+
750
+ // Try to find an invalid user
751
+ yield* Effect.logInfo("\nLooking up user -1...");
752
+ yield* Effect.gen(function* () {
753
+ const user = yield* findUser(-1);
754
+ yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
755
+ }).pipe(
756
+ Effect.catchTag("DatabaseError", (error) =>
757
+ Effect.logInfo(`Database error: ${error._tag} - ${error.cause}`)
758
+ )
759
+ );
760
+ });
761
+
762
+ // Run the program
763
+ Effect.runPromise(program);
764
+ ```
765
+
766
+ **Explanation:**
767
+ Tagged errors allow you to handle errors in a type-safe, self-documenting way.
768
+
769
+ **Anti-Pattern:**
770
+
771
+ Using generic `Error` objects or strings in the error channel. This loses all
772
+ type information, forcing consumers to use `catchAll` and perform unsafe
773
+ checks.
774
+
775
+ **Rationale:**
776
+
777
+ For any distinct failure mode in your application, define a custom error class
778
+ that extends `Data.TaggedError`.
779
+
780
+
781
+ This gives each error a unique, literal `_tag` that Effect can use for type
782
+ discrimination with `Effect.catchTag`, making your error handling fully
783
+ type-safe.
784
+
785
+ ---
786
+
787
+ ### Define Contracts Upfront with Schema
788
+
789
+ **Rule:** Define contracts upfront with schema.
790
+
791
+ **Good Example:**
792
+
793
+ ```typescript
794
+ import { Schema, Effect, Data } from "effect";
795
+
796
+ // Define User schema and type
797
+ const UserSchema = Schema.Struct({
798
+ id: Schema.Number,
799
+ name: Schema.String,
800
+ });
801
+
802
+ type User = Schema.Schema.Type<typeof UserSchema>;
803
+
804
+ // Define error type
805
+ class UserNotFound extends Data.TaggedError("UserNotFound")<{
806
+ readonly id: number;
807
+ }> {}
808
+
809
+ // Create database service implementation
810
+ export class Database extends Effect.Service<Database>()("Database", {
811
+ sync: () => ({
812
+ getUser: (id: number) =>
813
+ id === 1
814
+ ? Effect.succeed({ id: 1, name: "John" })
815
+ : Effect.fail(new UserNotFound({ id })),
816
+ }),
817
+ }) {}
818
+
819
+ // Create a program that demonstrates schema and error handling
820
+ const program = Effect.gen(function* () {
821
+ const db = yield* Database;
822
+
823
+ // Try to get an existing user
824
+ yield* Effect.logInfo("Looking up user 1...");
825
+ const user1 = yield* db.getUser(1);
826
+ yield* Effect.logInfo(`Found user: ${JSON.stringify(user1)}`);
827
+
828
+ // Try to get a non-existent user
829
+ yield* Effect.logInfo("\nLooking up user 999...");
830
+ yield* Effect.logInfo("Attempting to get user 999...");
831
+ yield* Effect.gen(function* () {
832
+ const user = yield* db.getUser(999);
833
+ yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
834
+ }).pipe(
835
+ Effect.catchAll((error) => {
836
+ if (error instanceof UserNotFound) {
837
+ return Effect.logInfo(`Error: User with id ${error.id} not found`);
838
+ }
839
+ return Effect.logInfo(`Unexpected error: ${error}`);
840
+ })
841
+ );
842
+
843
+ // Try to decode invalid data
844
+ yield* Effect.logInfo("\nTrying to decode invalid user data...");
845
+ const invalidUser = { id: "not-a-number", name: 123 } as any;
846
+ yield* Effect.gen(function* () {
847
+ const user = yield* Schema.decode(UserSchema)(invalidUser);
848
+ yield* Effect.logInfo(`Decoded user: ${JSON.stringify(user)}`);
849
+ }).pipe(
850
+ Effect.catchAll((error) =>
851
+ Effect.logInfo(`Validation failed:\n${JSON.stringify(error, null, 2)}`)
852
+ )
853
+ );
854
+ });
855
+
856
+ // Run the program
857
+ Effect.runPromise(Effect.provide(program, Database.Default));
858
+ ```
859
+
860
+ **Explanation:**
861
+ Defining schemas upfront clarifies your contracts and ensures both type safety
862
+ and runtime validation.
863
+
864
+ **Anti-Pattern:**
865
+
866
+ Defining logic with implicit `any` types first and adding validation later as
867
+ an afterthought. This leads to brittle code that lacks a clear contract.
868
+
869
+ **Rationale:**
870
+
871
+ Before writing implementation logic, define the shape of your data models and
872
+ function signatures using `Effect/Schema`.
873
+
874
+
875
+ This "schema-first" approach separates the "what" (the data shape) from the
876
+ "how" (the implementation). It provides a single source of truth for both
877
+ compile-time static types and runtime validation.
878
+
879
+ ---
880
+
881
+ ### Modeling Validated Domain Types with Brand
882
+
883
+ **Rule:** Use Brand to define types like Email, UserId, or PositiveInt, ensuring only valid values can be constructed and used.
884
+
885
+ **Good Example:**
886
+
887
+ ```typescript
888
+ import { Brand } from "effect";
889
+
890
+ // Define a branded type for Email
891
+ type Email = string & Brand.Brand<"Email">;
892
+
893
+ // Function that only accepts Email, not any string
894
+ function sendWelcome(email: Email) {
895
+ // ...
896
+ }
897
+
898
+ // Constructing an Email value (unsafe, see next pattern for validation)
899
+ const email = "user@example.com" as Email;
900
+
901
+ sendWelcome(email); // OK
902
+ // sendWelcome("not-an-email"); // Type error! (commented to allow compilation)
903
+ ```
904
+
905
+ **Explanation:**
906
+
907
+ - `Brand.Branded<T, Name>` creates a new type that is distinct from its base type.
908
+ - Only values explicitly branded as `Email` can be used where an `Email` is required.
909
+ - This prevents accidental mixing of domain types.
910
+
911
+ **Anti-Pattern:**
912
+
913
+ Using plain strings or numbers for domain-specific values (like emails, user IDs, or currency codes), which can lead to accidental misuse and bugs that are hard to catch.
914
+
915
+ **Rationale:**
916
+
917
+ Use the `Brand` utility to create domain-specific types from primitives like `string` or `number`.
918
+ This prevents accidental misuse and makes illegal states unrepresentable in your codebase.
919
+
920
+
921
+ Branded types add a layer of type safety, ensuring that values like `Email`, `UserId`, or `PositiveInt` are not confused with plain strings or numbers.
922
+ They help you catch bugs at compile time and make your code more self-documenting.
923
+
924
+ ---
925
+
926
+ ### Parse and Validate Data with Schema.decode
927
+
928
+ **Rule:** Parse and validate data with Schema.decode.
929
+
930
+ **Good Example:**
931
+
932
+ ```typescript
933
+ import { Effect, Schema } from "effect";
934
+
935
+ interface User {
936
+ name: string;
937
+ }
938
+
939
+ const UserSchema = Schema.Struct({
940
+ name: Schema.String,
941
+ }) as Schema.Schema<User>;
942
+
943
+ const processUserInput = (input: unknown) =>
944
+ Effect.gen(function* () {
945
+ const user = yield* Schema.decodeUnknown(UserSchema)(input);
946
+ return `Welcome, ${user.name}!​`;
947
+ }).pipe(
948
+ Effect.catchTag("ParseError", () => Effect.succeed("Invalid user data."))
949
+ );
950
+
951
+ // Demonstrate the schema parsing
952
+ const program = Effect.gen(function* () {
953
+ // Test with valid input
954
+ const validInput = { name: "Paul" };
955
+ const validResult = yield* processUserInput(validInput);
956
+ yield* Effect.logInfo(`Valid input result: ${validResult}`);
957
+
958
+ // Test with invalid input
959
+ const invalidInput = { age: 25 }; // Missing 'name' field
960
+ const invalidResult = yield* processUserInput(invalidInput);
961
+ yield* Effect.logInfo(`Invalid input result: ${invalidResult}`);
962
+
963
+ // Test with completely invalid input
964
+ const badInput = "not an object";
965
+ const badResult = yield* processUserInput(badInput);
966
+ yield* Effect.logInfo(`Bad input result: ${badResult}`);
967
+ });
968
+
969
+ Effect.runPromise(program);
970
+ ```
971
+
972
+ **Explanation:**
973
+ `Schema.decode` integrates parsing and validation into the Effect workflow,
974
+ making error handling composable and type-safe.
975
+
976
+ **Anti-Pattern:**
977
+
978
+ Using `Schema.parse(schema)(input)`, as it throws an exception. This forces
979
+ you to use `try/catch` blocks, which breaks the composability of Effect.
980
+
981
+ **Rationale:**
982
+
983
+ When you need to parse or validate data against a `Schema`, use the
984
+ `Schema.decode(schema)` function. It takes an `unknown` input and returns an
985
+ `Effect`.
986
+
987
+
988
+ Unlike the older `Schema.parse` which throws, `Schema.decode` is fully
989
+ integrated into the Effect ecosystem, allowing you to handle validation
990
+ failures gracefully with operators like `Effect.catchTag`.
991
+
992
+ ---
993
+
994
+ ### Validating and Parsing Branded Types
995
+
996
+ **Rule:** Combine Schema and Brand to validate and parse branded types, guaranteeing only valid domain values are created at runtime.
997
+
998
+ **Good Example:**
999
+
1000
+ ```typescript
1001
+ import { Brand, Effect, Schema } from "effect";
1002
+
1003
+ // Define a branded type for Email
1004
+ type Email = string & Brand.Brand<"Email">;
1005
+
1006
+ // Create a Schema for Email validation
1007
+ const EmailSchema = Schema.String.pipe(
1008
+ Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/), // Simple email regex
1009
+ Schema.brand("Email" as const) // Attach the brand
1010
+ );
1011
+
1012
+ // Parse and validate an email at runtime
1013
+ const parseEmail = (input: string) =>
1014
+ Effect.try({
1015
+ try: () => Schema.decodeSync(EmailSchema)(input),
1016
+ catch: (err) => `Invalid email: ${String(err)}`,
1017
+ });
1018
+
1019
+ // Usage
1020
+ parseEmail("user@example.com").pipe(
1021
+ Effect.match({
1022
+ onSuccess: (email) => console.log("Valid email:", email),
1023
+ onFailure: (err) => console.error(err),
1024
+ })
1025
+ );
1026
+ ```
1027
+
1028
+ **Explanation:**
1029
+
1030
+ - `Schema` is used to define validation logic for the branded type.
1031
+ - `Brand.schema<Email>()` attaches the brand to the schema, so only validated values can be constructed as `Email`.
1032
+ - This pattern ensures both compile-time and runtime safety.
1033
+
1034
+ **Anti-Pattern:**
1035
+
1036
+ Branding values without runtime validation, or accepting unvalidated user input as branded types, which can lead to invalid domain values and runtime bugs.
1037
+
1038
+ **Rationale:**
1039
+
1040
+ Use `Schema` in combination with `Brand` to validate and parse branded types at runtime.
1041
+ This ensures that only values passing your validation logic can be constructed as branded types, making your domain models robust and type-safe.
1042
+
1043
+
1044
+ While branding types at the type level prevents accidental misuse, runtime validation is needed to ensure only valid values are constructed from user input, APIs, or external sources.
1045
+
1046
+ ---
1047
+
1048
+ ### Avoid Long Chains of .andThen; Use Generators Instead
1049
+
1050
+ **Rule:** Prefer generators over long chains of .andThen.
1051
+
1052
+ **Good Example:**
1053
+
1054
+ ```typescript
1055
+ import { Effect } from "effect";
1056
+
1057
+ // Define our steps with logging
1058
+ const step1 = (): Effect.Effect<number> =>
1059
+ Effect.succeed(42).pipe(Effect.tap((n) => Effect.log(`Step 1: ${n}`)));
1060
+
1061
+ const step2 = (a: number): Effect.Effect<string> =>
1062
+ Effect.succeed(`Result: ${a * 2}`).pipe(
1063
+ Effect.tap((s) => Effect.log(`Step 2: ${s}`))
1064
+ );
1065
+
1066
+ // Using Effect.gen for better readability
1067
+ const program = Effect.gen(function* () {
1068
+ const a = yield* step1();
1069
+ const b = yield* step2(a);
1070
+ return b;
1071
+ });
1072
+
1073
+ // Run the program
1074
+ const programWithLogging = Effect.gen(function* () {
1075
+ const result = yield* program;
1076
+ yield* Effect.log(`Final result: ${result}`);
1077
+ return result;
1078
+ });
1079
+
1080
+ Effect.runPromise(programWithLogging);
1081
+ ```
1082
+
1083
+ **Explanation:**
1084
+ Generators keep sequential logic readable and easy to maintain.
1085
+
1086
+ **Anti-Pattern:**
1087
+
1088
+ ```typescript
1089
+ import { Effect } from "effect";
1090
+ declare const step1: () => Effect.Effect<any>;
1091
+ declare const step2: (a: any) => Effect.Effect<any>;
1092
+
1093
+ step1().pipe(Effect.flatMap((a) => step2(a))); // Or .andThen
1094
+ ```
1095
+
1096
+ Chaining many `.flatMap` or `.andThen` calls leads to deeply nested,
1097
+ hard-to-read code.
1098
+
1099
+ **Rationale:**
1100
+
1101
+ For sequential logic involving more than two steps, prefer `Effect.gen` over
1102
+ chaining multiple `.andThen` or `.flatMap` calls.
1103
+
1104
+
1105
+ `Effect.gen` provides a flat, linear code structure that is easier to read and
1106
+ debug than deeply nested functional chains.
1107
+
1108
+ ---
1109
+
1110
+ ### Distinguish 'Not Found' from Errors
1111
+
1112
+ **Rule:** Use Effect<Option<A>> to distinguish between recoverable 'not found' cases and actual failures.
1113
+
1114
+ **Good Example:**
1115
+
1116
+ This function to find a user can fail if the database is down, or it can succeed but find no user. The return type `Effect.Effect<Option.Option<User>, DatabaseError>` makes this contract perfectly clear.
1117
+
1118
+ ```typescript
1119
+ import { Effect, Option, Data } from "effect";
1120
+
1121
+ interface User {
1122
+ id: number;
1123
+ name: string;
1124
+ }
1125
+ class DatabaseError extends Data.TaggedError("DatabaseError") {}
1126
+
1127
+ // This signature is extremely honest about its possible outcomes.
1128
+ const findUserInDb = (
1129
+ id: number
1130
+ ): Effect.Effect<Option.Option<User>, DatabaseError> =>
1131
+ Effect.gen(function* () {
1132
+ // This could fail with a DatabaseError
1133
+ const dbResult = yield* Effect.try({
1134
+ try: () => (id === 1 ? { id: 1, name: "Paul" } : null),
1135
+ catch: () => new DatabaseError(),
1136
+ });
1137
+
1138
+ // We wrap the potentially null result in an Option
1139
+ return Option.fromNullable(dbResult);
1140
+ });
1141
+
1142
+ // The caller can now handle all three cases explicitly.
1143
+ const program = (id: number) =>
1144
+ findUserInDb(id).pipe(
1145
+ Effect.flatMap((maybeUser) =>
1146
+ Option.match(maybeUser, {
1147
+ onNone: () =>
1148
+ Effect.logInfo(`Result: User with ID ${id} was not found.`),
1149
+ onSome: (user) => Effect.logInfo(`Result: Found user ${user.name}.`),
1150
+ })
1151
+ ),
1152
+ Effect.catchAll((error) =>
1153
+ Effect.logInfo("Error: Could not connect to the database.")
1154
+ )
1155
+ );
1156
+
1157
+ // Run the program with different IDs
1158
+ Effect.runPromise(
1159
+ Effect.gen(function* () {
1160
+ // Try with existing user
1161
+ yield* Effect.logInfo("Looking for user with ID 1...");
1162
+ yield* program(1);
1163
+
1164
+ // Try with non-existent user
1165
+ yield* Effect.logInfo("\nLooking for user with ID 2...");
1166
+ yield* program(2);
1167
+ })
1168
+ );
1169
+ ```
1170
+
1171
+ **Anti-Pattern:**
1172
+
1173
+ A common alternative is to create a specific NotFoundError and put it in the error channel alongside other errors.
1174
+
1175
+ ```typescript
1176
+ class NotFoundError extends Data.TaggedError("NotFoundError") {}
1177
+
1178
+ // ❌ This signature conflates two different kinds of failure.
1179
+ const findUserUnsafely = (
1180
+ id: number
1181
+ ): Effect.Effect<User, DatabaseError | NotFoundError> => {
1182
+ // ...
1183
+ return Effect.fail(new NotFoundError());
1184
+ };
1185
+ ```
1186
+
1187
+ While this works, it can be less expressive. It treats a "not found" result—which might be a normal part of your application's flow—the same as a catastrophic DatabaseError.
1188
+
1189
+ Using `Effect<Option<A>>` often leads to clearer and more precise business logic.
1190
+
1191
+ **Rationale:**
1192
+
1193
+ When a computation can fail (e.g., a network error) or succeed but find nothing, model its return type as `Effect<Option<A>>`. This separates the "hard failure" channel from the "soft failure" (or empty) channel.
1194
+
1195
+ ---
1196
+
1197
+
1198
+ This pattern provides a precise way to handle three distinct outcomes of an operation:
1199
+
1200
+ 1. **Success with a value:** `Effect.succeed(Option.some(value))`
1201
+ 2. **Success with no value:** `Effect.succeed(Option.none())` (e.g., user not found)
1202
+ 3. **Failure:** `Effect.fail(new DatabaseError())` (e.g., database connection lost)
1203
+
1204
+ By using `Option` inside the success channel of an `Effect`, you keep the error channel clean for true, unexpected, or unrecoverable errors. The "not found" case is often an expected and recoverable part of your business logic, and `Option.none()` models this perfectly.
1205
+
1206
+ ---
1207
+
1208
+ ---
1209
+
1210
+ ### Model Validated Domain Types with Brand
1211
+
1212
+ **Rule:** Model validated domain types with Brand.
1213
+
1214
+ **Good Example:**
1215
+
1216
+ ```typescript
1217
+ import { Brand, Option } from "effect";
1218
+
1219
+ type Email = string & Brand.Brand<"Email">;
1220
+
1221
+ const makeEmail = (s: string): Option.Option<Email> =>
1222
+ s.includes("@") ? Option.some(s as Email) : Option.none();
1223
+
1224
+ // A function can now trust that its input is a valid email.
1225
+ const sendEmail = (email: Email, body: string) => {
1226
+ /* ... */
1227
+ };
1228
+ ```
1229
+
1230
+ **Explanation:**
1231
+ Branding ensures that only validated values are used, reducing bugs and
1232
+ repetitive checks.
1233
+
1234
+ **Anti-Pattern:**
1235
+
1236
+ "Primitive obsession"—using raw primitives (`string`, `number`) and performing
1237
+ validation inside every function that uses them. This is repetitive and
1238
+ error-prone.
1239
+
1240
+ **Rationale:**
1241
+
1242
+ For domain primitives that have specific rules (e.g., a valid email), create a
1243
+ Branded Type. This ensures a value can only be created after passing a
1244
+ validation check.
1245
+
1246
+
1247
+ This pattern moves validation to the boundaries of your system. Once a value
1248
+ has been branded, the rest of your application can trust that it is valid,
1249
+ eliminating repetitive checks.
1250
+
1251
+ ---
1252
+
1253
+ ### Accumulate Multiple Errors with Either
1254
+
1255
+ **Rule:** Use Either to accumulate multiple validation errors instead of failing on the first one.
1256
+
1257
+ **Good Example:**
1258
+
1259
+ Using `Schema.decode` with the `allErrors: true` option demonstrates this pattern perfectly. The underlying mechanism uses `Either` to collect all parsing errors into an array instead of stopping at the first one.
1260
+
1261
+ ```typescript
1262
+ import { Effect, Schema, Data, Either } from "effect";
1263
+
1264
+ // Define validation error type
1265
+ class ValidationError extends Data.TaggedError("ValidationError")<{
1266
+ readonly field: string;
1267
+ readonly message: string;
1268
+ }> {}
1269
+
1270
+ // Define user type
1271
+ type User = {
1272
+ name: string;
1273
+ email: string;
1274
+ };
1275
+
1276
+ // Define schema with custom validation
1277
+ const UserSchema = Schema.Struct({
1278
+ name: Schema.String.pipe(
1279
+ Schema.minLength(3),
1280
+ Schema.filter((name) => /^[A-Za-z\s]+$/.test(name), {
1281
+ message: () => "name must contain only letters and spaces",
1282
+ })
1283
+ ),
1284
+ email: Schema.String.pipe(
1285
+ Schema.pattern(/@/),
1286
+ Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
1287
+ message: () => "email must be a valid email address",
1288
+ })
1289
+ ),
1290
+ });
1291
+
1292
+ // Example inputs
1293
+ const invalidInputs: User[] = [
1294
+ {
1295
+ name: "Al", // Too short
1296
+ email: "bob-no-at-sign.com", // Invalid pattern
1297
+ },
1298
+ {
1299
+ name: "John123", // Contains numbers
1300
+ email: "john@incomplete", // Invalid email
1301
+ },
1302
+ {
1303
+ name: "Alice Smith", // Valid
1304
+ email: "alice@example.com", // Valid
1305
+ },
1306
+ ];
1307
+
1308
+ // Validate a single user
1309
+ const validateUser = (input: User) =>
1310
+ Effect.gen(function* () {
1311
+ const result = yield* Schema.decode(UserSchema)(input, { errors: "all" });
1312
+ return result;
1313
+ });
1314
+
1315
+ // Process multiple users and accumulate all errors
1316
+ const program = Effect.gen(function* () {
1317
+ yield* Effect.log("Validating users...\n");
1318
+
1319
+ for (const input of invalidInputs) {
1320
+ const result = yield* Effect.either(validateUser(input));
1321
+
1322
+ yield* Effect.log(`Validating user: ${input.name} <${input.email}>`);
1323
+
1324
+ // Handle success and failure cases separately for clarity
1325
+ // Using Either.match which is the idiomatic way to handle Either values
1326
+ yield* Either.match(result, {
1327
+ onLeft: (error) =>
1328
+ Effect.gen(function* () {
1329
+ yield* Effect.log("❌ Validation failed:");
1330
+ yield* Effect.log(error.message);
1331
+ yield* Effect.log(""); // Empty line for readability
1332
+ }),
1333
+ onRight: (user) =>
1334
+ Effect.gen(function* () {
1335
+ yield* Effect.log(`✅ User is valid: ${JSON.stringify(user)}`);
1336
+ yield* Effect.log(""); // Empty line for readability
1337
+ }),
1338
+ });
1339
+ }
1340
+ });
1341
+
1342
+ // Run the program
1343
+ Effect.runSync(program);
1344
+ ```
1345
+
1346
+ ---
1347
+
1348
+ **Anti-Pattern:**
1349
+
1350
+ Using `Effect`'s error channel for validation that requires multiple error messages. The code below will only ever report the first error it finds, because `Effect.fail` short-circuits the entire `Effect.gen` block.
1351
+
1352
+ ```typescript
1353
+ import { Effect } from "effect";
1354
+
1355
+ const validateWithEffect = (input: { name: string; email: string }) =>
1356
+ Effect.gen(function* () {
1357
+ if (input.name.length < 3) {
1358
+ // The program will fail here and never check the email.
1359
+ return yield* Effect.fail("Name is too short.");
1360
+ }
1361
+ if (!input.email.includes("@")) {
1362
+ return yield* Effect.fail("Email is invalid.");
1363
+ }
1364
+ return yield* Effect.succeed(input);
1365
+ });
1366
+ ```
1367
+
1368
+ **Rationale:**
1369
+
1370
+ When you need to perform multiple validation checks and collect all failures, use the `Either<E, A>` data type. `Either` represents a value that can be one of two possibilities: a `Left<E>` (typically for failure) or a `Right<A>` (typically for success).
1371
+
1372
+ ---
1373
+
1374
+
1375
+ The `Effect` error channel is designed to short-circuit. The moment an `Effect` fails, the entire computation stops and the error is propagated. This is perfect for handling unrecoverable errors like a lost database connection.
1376
+
1377
+ However, for tasks like validating a user's input, this is poor user experience. You want to show the user all of their mistakes at once.
1378
+
1379
+ `Either` is the solution. Since it's a pure data structure, you can run multiple checks that each return an `Either`, and then combine the results to accumulate all the `Left` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation.
1380
+
1381
+ ---
1382
+
1383
+ ---
1384
+
1385
+