opencode-effect-enforcer 0.2.0

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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,914 @@
1
+ ---
2
+ name: effect-pattern-matching
3
+ description: Master Effect pattern matching using Data.TaggedEnum, $match, $is, Match.typeTags, and Effect.match. Avoid manual _tag checks and Effect.result patterns. Use this skill when working with discriminated unions, ADTs, or conditional logic based on tagged types.
4
+ ---
5
+
6
+ # Effect Pattern Matching Skill
7
+
8
+ Use this skill when working with discriminated unions, ADTs, conditional logic, or any type that uses `_tag` discrimination. Pattern matching provides exhaustive, type-safe alternatives to imperative conditionals.
9
+
10
+ ## Effect Source Reference
11
+
12
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
13
+ Browse and read files there directly to look up APIs, types, and implementations.
14
+
15
+ Reference this for:
16
+
17
+ - Match source: `packages/effect/src/Match.ts`
18
+ - Data source: `packages/effect/src/Data.ts`
19
+ - Full Schema API: `packages/effect/SCHEMA.md`
20
+ - Effect source: `packages/effect/src/`
21
+
22
+ ## Core Philosophy
23
+
24
+ **Pattern matching over imperative conditionals**:
25
+
26
+ - Exhaustive by default (compiler enforces all cases)
27
+ - Type-safe refinement in each branch
28
+ - Declarative, not imperative
29
+ - Pipeline-friendly composition
30
+
31
+ ## Pattern 1: Data.TaggedEnum for ADTs
32
+
33
+ Use `Data.TaggedEnum` instead of manual tagged unions.
34
+
35
+ ### The Problem: Manual Tagged Unions
36
+
37
+ ```typescript
38
+ // ❌ WRONG - Manual tagged union
39
+ type WalletState =
40
+ | { readonly _tag: 'Disconnected' }
41
+ | { readonly _tag: 'Connecting' }
42
+ | { readonly _tag: 'Connected'; readonly address: string }
43
+ | { readonly _tag: 'Error'; readonly message: string };
44
+
45
+ // Manual constructors - verbose and error-prone
46
+ const disconnected = (): WalletState => ({ _tag: 'Disconnected' });
47
+ const connecting = (): WalletState => ({ _tag: 'Connecting' });
48
+ const connected = (address: string): WalletState => ({
49
+ _tag: 'Connected',
50
+ address
51
+ });
52
+ const error = (message: string): WalletState => ({ _tag: 'Error', message });
53
+
54
+ // No built-in pattern matching
55
+ // No type guards
56
+ // No exhaustiveness checking
57
+ ```
58
+
59
+ ### The Solution: Data.TaggedEnum
60
+
61
+ ```typescript
62
+ // ✅ CORRECT - TaggedEnum with constructors + $match + $is
63
+ import { Data } from 'effect';
64
+
65
+ type WalletState = Data.TaggedEnum<{
66
+ Disconnected: {};
67
+ Connecting: {};
68
+ Connected: { readonly address: string };
69
+ Error: { readonly message: string };
70
+ }>;
71
+
72
+ const WalletState = Data.taggedEnum<WalletState>();
73
+
74
+ /**
75
+ * WalletState now provides:
76
+ * - WalletState.Disconnected() - Constructor
77
+ * - WalletState.Connecting() - Constructor
78
+ * - WalletState.Connected({ address }) - Constructor
79
+ * - WalletState.Error({ message }) - Constructor
80
+ * - WalletState.$match(state, { ... }) - Pattern matching
81
+ * - WalletState.$is("Connected")(state) - Type guard (`_tag` check only)
82
+ */
83
+
84
+ // Usage
85
+ const state = WalletState.Connected({ address: '0x123' });
86
+
87
+ // Pattern match
88
+ const display = WalletState.$match(state, {
89
+ Disconnected: () => 'Please connect wallet',
90
+ Connecting: () => 'Connecting...',
91
+ Connected: ({ address }) => `Connected: ${address}`,
92
+ Error: ({ message }) => `Error: ${message}`
93
+ });
94
+
95
+ // Type guard
96
+ if (WalletState.$is('Connected')(state)) {
97
+ console.log(state.address); // Type-safe access
98
+ }
99
+ ```
100
+
101
+ > **Caveat:** `Data.$is(tag)` / `TaggedEnum.$is(tag)` only checks the `_tag` field, not the full structure. Use it for values produced by your constructors; validate untrusted input with `Schema` before relying on `$is`.
102
+
103
+ ### Benefits of Data.TaggedEnum
104
+
105
+ 1. **Automatic constructors** - No manual factory functions
106
+ 2. **Automatic $match** - Exhaustive pattern matching built-in
107
+ 3. **Automatic $is** - Type-safe guards for each variant
108
+ 4. **Type inference** - Compiler knows all variants
109
+ 5. **Compile-time exhaustiveness** - Forget a case? Compiler error
110
+
111
+ ### When to Use Data.TaggedEnum
112
+
113
+ - **State machines**: Connection states, loading states, workflow states
114
+ - **Domain events**: UserLoggedIn, UserLoggedOut, SessionExpired
115
+ - **Command types**: CreateUser, UpdateUser, DeleteUser
116
+ - **Result types**: Success, Failure, Pending
117
+ - **Any discriminated union** with multiple variants
118
+
119
+ ## Pattern 2: Avoid Effect.result + \_tag Checks
120
+
121
+ Use `Effect.match` instead of `Effect.result` with manual tag checks.
122
+
123
+ ### The Problem: Effect.result with Manual Checks
124
+
125
+ ```typescript
126
+ // ❌ WRONG - Effect.result with manual _tag checks
127
+ import { Effect, Result, Schema } from 'effect';
128
+
129
+ declare const User: { name: string; id: string };
130
+ type User = typeof User;
131
+
132
+ class NotFound extends Schema.TaggedError<NotFound>()('NotFound', {
133
+ id: Schema.String
134
+ }) {}
135
+
136
+ const getUser = (id: string): Effect.Effect<User, NotFound> =>
137
+ Effect.fail(new NotFound({ id }));
138
+
139
+ const program = Effect.gen(function* () {
140
+ const result = yield* Effect.result(getUser('123'));
141
+
142
+ // Manual tag checking - not exhaustive
143
+ if (result._tag === 'Failure') {
144
+ console.error(`User not found: ${result.failure.id}`);
145
+ return null;
146
+ }
147
+
148
+ return result.success;
149
+ });
150
+ ```
151
+
152
+ **Problems:**
153
+
154
+ - Not exhaustive (could forget Success case)
155
+ - Verbose and imperative
156
+ - Breaks pipeline style
157
+ - Manual unwrapping of Either
158
+
159
+ ### The Solution: Effect.match
160
+
161
+ ```typescript
162
+ // ✅ CORRECT - Effect.match for declarative error handling
163
+ import { Effect, Schema } from 'effect';
164
+
165
+ declare const User: { name: string; id: string };
166
+ type User = typeof User;
167
+
168
+ class NotFound extends Schema.TaggedError<NotFound>()('NotFound', {
169
+ id: Schema.String
170
+ }) {}
171
+
172
+ const getUser = (id: string): Effect.Effect<User, NotFound> =>
173
+ Effect.fail(new NotFound({ id }));
174
+
175
+ const program = getUser('123').pipe(
176
+ Effect.match({
177
+ onFailure: (error) => {
178
+ console.error(`User not found: ${error.id}`);
179
+ return null;
180
+ },
181
+ onSuccess: (user) => user
182
+ })
183
+ );
184
+ ```
185
+
186
+ **Benefits:**
187
+
188
+ - Exhaustive (must handle both cases)
189
+ - Declarative and pipeline-friendly
190
+ - No manual Either unwrapping
191
+ - Type-safe refinement in each branch
192
+
193
+ ### Effect.match Variants
194
+
195
+ ```typescript
196
+ import { Effect, Cause } from 'effect';
197
+
198
+ declare const effect: Effect.Effect<unknown, unknown, unknown>;
199
+ declare function handleError(error: unknown): unknown;
200
+ declare function handleSuccess(value: unknown): unknown;
201
+ declare function handleCause(cause: Cause.Cause<unknown>): unknown;
202
+
203
+ // Basic match - transform both success and failure
204
+ Effect.match(effect, {
205
+ onFailure: (error) => handleError(error),
206
+ onSuccess: (value) => handleSuccess(value)
207
+ });
208
+
209
+ // matchEffect - return Effects from handlers
210
+ Effect.matchEffect(effect, {
211
+ onFailure: (error) => Effect.logError(error).pipe(Effect.as(null)),
212
+ onSuccess: (value) => Effect.succeed(value)
213
+ });
214
+
215
+ // matchCause - match on full Cause (errors + defects + interrupts)
216
+ Effect.matchCause(effect, {
217
+ onFailure: (cause) => handleCause(cause),
218
+ onSuccess: (value) => value
219
+ });
220
+
221
+ // matchCauseEffect - Cause matching with Effect handlers
222
+ Effect.matchCauseEffect(effect, {
223
+ onFailure: (cause) => Effect.logError(cause).pipe(Effect.as(null)),
224
+ onSuccess: (value) => Effect.succeed(value)
225
+ });
226
+ ```
227
+
228
+ ## Pattern 3: Use $match for Exhaustive Pattern Matching
229
+
230
+ Use `TaggedEnum.$match` for exhaustive, type-safe pattern matching.
231
+
232
+ ### The Problem: if/else Chains
233
+
234
+ ```typescript
235
+ // ❌ WRONG - if/else chains, not exhaustive
236
+ import { Data } from 'effect';
237
+
238
+ type Status = Data.TaggedEnum<{
239
+ Active: {};
240
+ Expired: {};
241
+ Revoked: {};
242
+ }>;
243
+ const Status = Data.taggedEnum<Status>();
244
+
245
+ const getColor = (status: Status): string => {
246
+ if (status._tag === 'Active') {
247
+ return 'green';
248
+ } else if (status._tag === 'Expired') {
249
+ return 'yellow';
250
+ }
251
+ // Forgot "Revoked" - no compiler error!
252
+ return 'gray';
253
+ };
254
+ ```
255
+
256
+ **Problems:**
257
+
258
+ - Not exhaustive (easy to forget cases)
259
+ - Compiler doesn't enforce completeness
260
+ - Imperative style
261
+ - Hard to refactor when adding variants
262
+
263
+ ### The Solution: $match
264
+
265
+ ```typescript
266
+ // ✅ CORRECT - $match with exhaustive checking
267
+ import { Data } from 'effect';
268
+
269
+ type Status = Data.TaggedEnum<{
270
+ Active: {};
271
+ Expired: {};
272
+ Revoked: {};
273
+ }>;
274
+ const Status = Data.taggedEnum<Status>();
275
+
276
+ const getColor = (status: Status): string =>
277
+ Status.$match(status, {
278
+ Active: () => 'green',
279
+ Expired: () => 'yellow',
280
+ Revoked: () => 'red'
281
+ // Compiler error if any case is missing!
282
+ });
283
+ ```
284
+
285
+ **Benefits:**
286
+
287
+ - **Exhaustive** - Compiler enforces all cases
288
+ - **Type-safe** - Each handler gets refined type
289
+ - **Declarative** - Clear mapping from variant to result
290
+ - **Refactor-safe** - Add variant? Compiler finds all matches to update
291
+
292
+ ### $match with Data Access
293
+
294
+ ```typescript
295
+ type AsyncState = Data.TaggedEnum<{
296
+ Idle: {};
297
+ Loading: {};
298
+ Success: { readonly data: string };
299
+ Failure: { readonly error: string };
300
+ }>;
301
+ const AsyncState = Data.taggedEnum<AsyncState>();
302
+
303
+ const display = (state: AsyncState): string =>
304
+ AsyncState.$match(state, {
305
+ Idle: () => 'Not started',
306
+ Loading: () => 'Loading...',
307
+ Success: ({ data }) => `Loaded: ${data}`,
308
+ Failure: ({ error }) => `Error: ${error}`
309
+ });
310
+ ```
311
+
312
+ ### Nested Pattern Matching
313
+
314
+ ```typescript
315
+ type Request = Data.TaggedEnum<{
316
+ Pending: {};
317
+ Approved: { readonly by: string };
318
+ Rejected: { readonly reason: string };
319
+ }>;
320
+ const Request = Data.taggedEnum<Request>();
321
+
322
+ type Workflow = Data.TaggedEnum<{
323
+ Draft: { readonly request: Request };
324
+ Submitted: { readonly request: Request };
325
+ Completed: {};
326
+ }>;
327
+ const Workflow = Data.taggedEnum<Workflow>();
328
+
329
+ const getStatus = (workflow: Workflow): string =>
330
+ Workflow.$match(workflow, {
331
+ Draft: ({ request }) =>
332
+ Request.$match(request, {
333
+ Pending: () => 'Draft - Pending',
334
+ Approved: ({ by }) => `Draft - Approved by ${by}`,
335
+ Rejected: ({ reason }) => `Draft - Rejected: ${reason}`
336
+ }),
337
+ Submitted: ({ request }) =>
338
+ Request.$match(request, {
339
+ Pending: () => 'Submitted - Awaiting approval',
340
+ Approved: ({ by }) => `Submitted - Approved by ${by}`,
341
+ Rejected: ({ reason }) => `Submitted - Rejected: ${reason}`
342
+ }),
343
+ Completed: () => 'Completed'
344
+ });
345
+ ```
346
+
347
+ ## Pattern 4: Use $is for Single-Case Type Guards
348
+
349
+ Use `TaggedEnum.$is` instead of manual `_tag` checks. It only checks `_tag`, so validate untrusted input with `Schema` before using it as a structural guarantee.
350
+
351
+ ### The Problem: Manual \_tag Checks
352
+
353
+ ```typescript
354
+ // ❌ WRONG - Manual tag checking
355
+ import { Data } from 'effect';
356
+
357
+ type Status = Data.TaggedEnum<{
358
+ Active: {};
359
+ Expired: {};
360
+ }>;
361
+ const Status = Data.taggedEnum<Status>();
362
+
363
+ // Verbose and repetitive
364
+ const status = Status.Active();
365
+ if (status._tag === 'Active') {
366
+ console.log('Active!');
367
+ }
368
+
369
+ // Hard to use in Array methods
370
+ const items: Status[] = [Status.Active(), Status.Expired()];
371
+ const activeItems = items.filter((item) => item._tag === 'Active');
372
+ ```
373
+
374
+ ### The Solution: $is Type Guards
375
+
376
+ ```typescript
377
+ // ✅ CORRECT - $is for type-safe guards
378
+ import { Data, Array, pipe } from 'effect';
379
+
380
+ type Status = Data.TaggedEnum<{
381
+ Active: {};
382
+ Expired: {};
383
+ }>;
384
+ const Status = Data.taggedEnum<Status>();
385
+
386
+ const status = Status.Active();
387
+
388
+ // Clean, declarative guard
389
+ if (Status.$is('Active')(status)) {
390
+ console.log('Active!');
391
+ }
392
+
393
+ // Perfect for Array methods
394
+ const items: Status[] = [Status.Active(), Status.Expired()];
395
+ const activeItems = items.filter(Status.$is('Active'));
396
+
397
+ // Pipeline-friendly
398
+ const hasActive = pipe(items, Array.some(Status.$is('Active')));
399
+
400
+ // Multiple guards
401
+ const activeOrExpired = items.filter(
402
+ (item) => Status.$is('Active')(item) || Status.$is('Expired')(item)
403
+ );
404
+ ```
405
+
406
+ ### $is in Effect Pipelines
407
+
408
+ ```typescript
409
+ import { Data, pipe } from 'effect';
410
+
411
+ type LoadState = Data.TaggedEnum<{
412
+ Loading: {};
413
+ Ready: { readonly data: string[] };
414
+ Error: { readonly message: string };
415
+ }>;
416
+ const LoadState = Data.taggedEnum<LoadState>();
417
+
418
+ const getData = (state: LoadState): string[] =>
419
+ pipe(
420
+ state,
421
+ // Type guard refines to Ready
422
+ LoadState.$is('Ready'),
423
+ // Now can access .data safely
424
+ (ready) => (ready ? ready.data : [])
425
+ );
426
+ ```
427
+
428
+ ## Pattern 5: Use Option.match Instead of \_tag Checks
429
+
430
+ Use `Option.match` instead of manual `._tag` checks on Options.
431
+
432
+ ### The Problem: Manual Option Tag Checks
433
+
434
+ ```typescript
435
+ // ❌ WRONG - Manual Option._tag checks
436
+ import { Option } from 'effect';
437
+
438
+ type User = { name: string; id: string };
439
+
440
+ const maybeUser: Option.Option<User> = Option.some({
441
+ name: 'Alice',
442
+ id: '123'
443
+ });
444
+
445
+ // Imperative and verbose
446
+ if (maybeUser._tag === 'Some') {
447
+ console.log(maybeUser.value.name);
448
+ } else {
449
+ console.log('No user');
450
+ }
451
+ ```
452
+
453
+ ### The Solution: Option.match
454
+
455
+ ```typescript
456
+ // ✅ CORRECT - Option.match
457
+ import { Option, pipe } from 'effect';
458
+
459
+ type User = { name: string; id: string };
460
+
461
+ const maybeUser: Option.Option<User> = Option.some({
462
+ name: 'Alice',
463
+ id: '123'
464
+ });
465
+
466
+ const display = Option.match(maybeUser, {
467
+ onNone: () => 'No user',
468
+ onSome: (user) => user.name
469
+ });
470
+
471
+ // In pipelines
472
+ const name = pipe(
473
+ maybeUser,
474
+ Option.match({
475
+ onNone: () => 'Guest',
476
+ onSome: (user) => user.name
477
+ })
478
+ );
479
+ ```
480
+
481
+ ### Option Pattern Matching Variants
482
+
483
+ ```typescript
484
+ import { Option, pipe } from 'effect';
485
+
486
+ declare const option: Option.Option<string>;
487
+ declare const defaultValue: string;
488
+ declare function transform(value: string): string;
489
+ declare function predicate(value: string): boolean;
490
+
491
+ // Basic match
492
+ Option.match(option, {
493
+ onNone: () => defaultValue,
494
+ onSome: (value) => transform(value)
495
+ });
496
+
497
+ // getOrElse - simpler for just default value
498
+ Option.getOrElse(option, () => defaultValue);
499
+
500
+ // map + getOrElse pattern
501
+ pipe(
502
+ option,
503
+ Option.map(transform),
504
+ Option.getOrElse(() => defaultValue)
505
+ );
506
+
507
+ // filter + match
508
+ pipe(
509
+ option,
510
+ Option.filter(predicate),
511
+ Option.match({
512
+ onNone: () => 'Filtered out or was None',
513
+ onSome: (value) => `Matched: ${value}`
514
+ })
515
+ );
516
+ ```
517
+
518
+ ## Pattern 6: Use Match.typeTags for Schema Unions
519
+
520
+ For Schema-based unions, use `Match.typeTags` for pattern matching.
521
+
522
+ ### Schema Union Pattern Matching
523
+
524
+ ```typescript
525
+ import { Schema, Match } from 'effect';
526
+
527
+ // Schema-based tagged structs
528
+ const Admin = Schema.TaggedStruct('Admin', {
529
+ id: Schema.String,
530
+ permissions: Schema.Array(Schema.String)
531
+ });
532
+
533
+ const Customer = Schema.TaggedStruct('Customer', {
534
+ id: Schema.String,
535
+ tier: Schema.Literals(['free', 'premium'])
536
+ });
537
+
538
+ const User = Schema.Union([Admin, Customer]);
539
+ type User = Schema.Schema.Type<typeof User>;
540
+
541
+ // Match.typeTags for Schema unions
542
+ const getPermissions = Match.typeTags<User>()({
543
+ Admin: ({ permissions }) => permissions,
544
+ Customer: ({ tier }) => (tier === 'premium' ? ['read'] : [])
545
+ });
546
+
547
+ const user: User = {
548
+ _tag: 'Admin' as const,
549
+ id: '1',
550
+ permissions: ['read', 'write']
551
+ };
552
+
553
+ const perms = getPermissions(user); // ["read", "write"]
554
+ ```
555
+
556
+ ### Match.typeTags Pattern
557
+
558
+ ```typescript
559
+ import { Match, Data } from 'effect';
560
+
561
+ type UnionType = Data.TaggedEnum<{
562
+ VariantA: { field: string };
563
+ VariantB: { other: number };
564
+ }>;
565
+
566
+ declare const value: UnionType;
567
+ declare function handleA(data: { field: string }): string;
568
+ declare function handleB(data: { other: number }): string;
569
+
570
+ // Create matcher function
571
+ const match = Match.typeTags<UnionType>();
572
+
573
+ // Use with handlers object
574
+ const result = match({
575
+ VariantA: (data) => handleA(data),
576
+ VariantB: (data) => handleB(data)
577
+ })(value);
578
+
579
+ // Or create matcher and apply later
580
+ const matcher = match({
581
+ VariantA: (data) => handleA(data),
582
+ VariantB: (data) => handleB(data)
583
+ });
584
+ const result2 = matcher(value);
585
+ ```
586
+
587
+ ## Pattern 7: Match.fn for Selector-Based Functions
588
+
589
+ Use `Match.fn(selector)` when a reusable function takes several arguments or should match a projection of its input. The selector chooses the value matched by `when` / `tag` / other cases, while the compiled matcher preserves the selector's original argument list.
590
+
591
+ Case handlers receive the narrowed selected value first, followed by all original selector arguments:
592
+
593
+ ```typescript
594
+ import { Match } from 'effect';
595
+
596
+ type Todo = {
597
+ readonly status: 'Active' | 'Completed';
598
+ readonly title: string;
599
+ };
600
+
601
+ const formatTodo = Match.fn((prefix: string, todo: Todo) => todo.status).pipe(
602
+ Match.when(
603
+ 'Active',
604
+ (_status, prefix, todo) => `${prefix}: active ${todo.title}`
605
+ ),
606
+ Match.when(
607
+ 'Completed',
608
+ (_status, prefix, todo) => `${prefix}: completed ${todo.title}`
609
+ ),
610
+ Match.exhaustive
611
+ );
612
+
613
+ formatTodo('Todo', { status: 'Active', title: 'Write tests' });
614
+ // "Todo: active Write tests"
615
+ ```
616
+
617
+ `Match.exhaustive`, `Match.orElse`, `Match.option`, and `Match.result` all compile a `Match.fn` matcher to a function with the selector's original parameters. Prefer `Match.type<I>()` when the matched value itself is the function's only input; use `Match.fn` when matching one argument, a derived property, or another projection while retaining surrounding arguments.
618
+
619
+ ## Pattern 8: Loadable.match for Async State
620
+
621
+ Use `Loadable.match` for async state pattern matching.
622
+
623
+ ### Loadable Pattern
624
+
625
+ ```tsx
626
+ import { Loadable } from '@/typeclass/Loadable';
627
+
628
+ type User = { name: string; id: string };
629
+
630
+ declare const Spinner: () => JSX.Element;
631
+ declare const UserProfile: (props: { user: User }) => JSX.Element;
632
+ declare const ErrorDisplay: (props: { error: Error }) => JSX.Element;
633
+
634
+ type UserData = Loadable.Loadable<User>;
635
+
636
+ const display = (data: UserData): JSX.Element =>
637
+ Loadable.match(data, {
638
+ onPending: () => <Spinner />,
639
+ onReady: (user) => <UserProfile user={user} />
640
+ });
641
+
642
+ // With error state
643
+ type UserDataWithError = Loadable.LoadableWithError<User, Error>;
644
+
645
+ const displayWithError = (data: UserDataWithError): JSX.Element =>
646
+ Loadable.matchWithError(data, {
647
+ onPending: () => <Spinner />,
648
+ onReady: (user) => <UserProfile user={user} />,
649
+ onError: (error) => <ErrorDisplay error={error} />
650
+ });
651
+ ```
652
+
653
+ ## Testability: Effect Services
654
+
655
+ When pattern matching involves non-deterministic operations, use Effect services.
656
+
657
+ ### The Problem: Untestable Direct Calls
658
+
659
+ ```typescript
660
+ // ❌ WRONG - untestable
661
+ import { Data } from 'effect';
662
+
663
+ type State = Data.TaggedEnum<{
664
+ Active: {};
665
+ Expired: {};
666
+ }>;
667
+ const State = Data.taggedEnum<State>();
668
+
669
+ const processState = (state: State): string =>
670
+ State.$match(state, {
671
+ Active: () => `Active at ${Date.now()}`,
672
+ Expired: () => `Expired at ${Date.now()}`
673
+ });
674
+ ```
675
+
676
+ ### The Solution: Effect Services
677
+
678
+ ```typescript
679
+ // ✅ CORRECT - testable with Clock service
680
+ import { Clock, Effect, Data } from 'effect';
681
+ import { TestClock } from 'effect/testing';
682
+
683
+ type State = Data.TaggedEnum<{
684
+ Active: {};
685
+ Expired: {};
686
+ }>;
687
+ const State = Data.taggedEnum<State>();
688
+
689
+ const processState = (state: State): Effect.Effect<string> =>
690
+ Effect.gen(function* () {
691
+ const now = yield* Clock.currentTimeMillis;
692
+
693
+ return State.$match(state, {
694
+ Active: () => `Active at ${now}`,
695
+ Expired: () => `Expired at ${now}`
696
+ });
697
+ });
698
+
699
+ // In tests, use TestClock for deterministic time
700
+ const testProgram = processState(State.Active()).pipe(
701
+ Effect.provide(TestClock.layer())
702
+ );
703
+ ```
704
+
705
+ ### Random Values in Pattern Matching
706
+
707
+ ```typescript
708
+ // ❌ WRONG - untestable
709
+ import { Data } from 'effect';
710
+
711
+ type User = Data.TaggedEnum<{
712
+ Admin: {};
713
+ Customer: {};
714
+ }>;
715
+ const User = Data.taggedEnum<User>();
716
+
717
+ const assignColor = (user: User): string =>
718
+ User.$match(user, {
719
+ Admin: () => 'red',
720
+ Customer: () => (Math.random() > 0.5 ? 'blue' : 'green')
721
+ });
722
+
723
+ // ✅ CORRECT - testable with Random service
724
+ import { Random, Effect } from 'effect';
725
+
726
+ const assignColorTestable = (user: User): Effect.Effect<string> =>
727
+ User.$match(user, {
728
+ Admin: () => Effect.succeed('red'),
729
+ Customer: () =>
730
+ Effect.gen(function* () {
731
+ const rand = yield* Random.next;
732
+ return rand > 0.5 ? 'blue' : 'green';
733
+ })
734
+ });
735
+ ```
736
+
737
+ ## Complete Example: Wallet Connection State Machine
738
+
739
+ ```typescript
740
+ import { Data, Effect, Clock } from 'effect';
741
+
742
+ // Define state machine with TaggedEnum
743
+ type WalletState = Data.TaggedEnum<{
744
+ Disconnected: {};
745
+ Connecting: { readonly startedAt: number };
746
+ Connected: {
747
+ readonly address: string;
748
+ readonly connectedAt: number;
749
+ };
750
+ Error: {
751
+ readonly message: string;
752
+ readonly occurredAt: number;
753
+ };
754
+ }>;
755
+
756
+ const WalletState = Data.taggedEnum<WalletState>();
757
+
758
+ // State transitions
759
+ const connect = (): Effect.Effect<WalletState> =>
760
+ Effect.gen(function* () {
761
+ const now = yield* Clock.currentTimeMillis;
762
+ return WalletState.Connecting({ startedAt: now });
763
+ });
764
+
765
+ const completeConnection = (address: string): Effect.Effect<WalletState> =>
766
+ Effect.gen(function* () {
767
+ const now = yield* Clock.currentTimeMillis;
768
+ return WalletState.Connected({
769
+ address,
770
+ connectedAt: now
771
+ });
772
+ });
773
+
774
+ const fail = (message: string): Effect.Effect<WalletState> =>
775
+ Effect.gen(function* () {
776
+ const now = yield* Clock.currentTimeMillis;
777
+ return WalletState.Error({
778
+ message,
779
+ occurredAt: now
780
+ });
781
+ });
782
+
783
+ // Pattern match for display
784
+ const displayState = (state: WalletState): string =>
785
+ WalletState.$match(state, {
786
+ Disconnected: () => 'Please connect your wallet',
787
+ Connecting: ({ startedAt }) =>
788
+ `Connecting... (started at ${startedAt})`,
789
+ Connected: ({ address, connectedAt }) =>
790
+ `Connected: ${address} (at ${connectedAt})`,
791
+ Error: ({ message, occurredAt }) =>
792
+ `Error: ${message} (at ${occurredAt})`
793
+ });
794
+
795
+ // Type-safe state queries using $is
796
+ const isConnected = WalletState.$is('Connected');
797
+ const canDisconnect = (state: WalletState): boolean =>
798
+ isConnected(state) || WalletState.$is('Error')(state);
799
+
800
+ // Filter connected states
801
+ const getConnectedStates = (
802
+ states: WalletState[]
803
+ ): Array<Extract<WalletState, { _tag: 'Connected' }>> =>
804
+ states.filter(isConnected);
805
+ ```
806
+
807
+ ## Quality Checklist
808
+
809
+ Before completing pattern matching implementation:
810
+
811
+ - [ ] Use `Data.TaggedEnum` for ADTs (not manual tagged unions)
812
+ - [ ] Use `TaggedEnum.$match` for exhaustive matching
813
+ - [ ] Use `TaggedEnum.$is` for type guards (not `._tag === `)
814
+ - [ ] Use `Effect.match` instead of `Effect.result` + if checks
815
+ - [ ] Use `Option.match` instead of `Option._tag` checks
816
+ - [ ] Use `Match.typeTags` for Schema union matching
817
+ - [ ] Use `Match.fn` when matching a selector while preserving multiple function arguments
818
+ - [ ] All pattern matches are exhaustive (compiler-checked)
819
+ - [ ] Use `Clock` service instead of `Date.now()` in matches
820
+ - [ ] Use `Random` service instead of `Math.random()` in matches
821
+ - [ ] Pattern matching is declarative (no imperative conditionals)
822
+ - [ ] Pipeline-friendly composition
823
+ - [ ] Type-safe refinement in each branch
824
+
825
+ ## Common Patterns Summary
826
+
827
+ ### ADT Definition
828
+
829
+ ```typescript
830
+ import { Data } from 'effect';
831
+
832
+ type State = Data.TaggedEnum<{
833
+ VariantA: { field: string };
834
+ VariantB: { other: number };
835
+ }>;
836
+ const State = Data.taggedEnum<State>();
837
+ ```
838
+
839
+ ### Exhaustive Matching
840
+
841
+ ```typescript
842
+ import { Data } from 'effect';
843
+
844
+ type State = Data.TaggedEnum<{
845
+ VariantA: { field: string };
846
+ VariantB: { other: number };
847
+ }>;
848
+ const State = Data.taggedEnum<State>();
849
+
850
+ declare const state: State;
851
+ declare function handleA(field: string): void;
852
+ declare function handleB(other: number): void;
853
+
854
+ State.$match(state, {
855
+ VariantA: ({ field }) => handleA(field),
856
+ VariantB: ({ other }) => handleB(other)
857
+ });
858
+ ```
859
+
860
+ ### Type Guards
861
+
862
+ ```typescript
863
+ import { Data } from 'effect';
864
+
865
+ type State = Data.TaggedEnum<{
866
+ VariantA: { field: string };
867
+ VariantB: { other: number };
868
+ }>;
869
+ const State = Data.taggedEnum<State>();
870
+
871
+ declare const state: State;
872
+ declare const items: State[];
873
+
874
+ if (State.$is('VariantA')(state)) {
875
+ // state is refined to VariantA
876
+ }
877
+
878
+ // In filters
879
+ items.filter(State.$is('VariantA'));
880
+ ```
881
+
882
+ ### Effect Matching
883
+
884
+ ```typescript
885
+ import { Effect } from 'effect';
886
+
887
+ declare const effect: Effect.Effect<unknown, unknown, unknown>;
888
+ declare function handleError(error: unknown): unknown;
889
+ declare function handleSuccess(value: unknown): unknown;
890
+
891
+ effect.pipe(
892
+ Effect.match({
893
+ onFailure: (error) => handleError(error),
894
+ onSuccess: (value) => handleSuccess(value)
895
+ })
896
+ );
897
+ ```
898
+
899
+ ### Option Matching
900
+
901
+ ```typescript
902
+ import { Option } from 'effect';
903
+
904
+ declare const option: Option.Option<string>;
905
+ declare const defaultValue: string;
906
+ declare function transform(value: string): string;
907
+
908
+ Option.match(option, {
909
+ onNone: () => defaultValue,
910
+ onSome: (value) => transform(value)
911
+ });
912
+ ```
913
+
914
+ Your pattern matching implementations should be exhaustive, type-safe, declarative, and testable.