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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- 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.
|