@jagreehal/workflow 1.12.0 → 1.13.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/README.md +1197 -20
- package/dist/duration.cjs +2 -0
- package/dist/duration.cjs.map +1 -0
- package/dist/duration.d.cts +246 -0
- package/dist/duration.d.ts +246 -0
- package/dist/duration.js +2 -0
- package/dist/duration.js.map +1 -0
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/match.cjs +2 -0
- package/dist/match.cjs.map +1 -0
- package/dist/match.d.cts +216 -0
- package/dist/match.d.ts +216 -0
- package/dist/match.js +2 -0
- package/dist/match.js.map +1 -0
- package/dist/schedule.cjs +2 -0
- package/dist/schedule.cjs.map +1 -0
- package/dist/schedule.d.cts +387 -0
- package/dist/schedule.d.ts +387 -0
- package/dist/schedule.js +2 -0
- package/dist/schedule.js.map +1 -0
- package/docs/api.md +30 -0
- package/docs/coming-from-neverthrow.md +103 -10
- package/docs/effect-features-to-port.md +210 -0
- package/docs/match-examples.test.ts +558 -0
- package/docs/match.md +417 -0
- package/docs/policies-examples.test.ts +750 -0
- package/docs/policies.md +508 -0
- package/docs/resource-management-examples.test.ts +729 -0
- package/docs/resource-management.md +509 -0
- package/docs/schedule-examples.test.ts +736 -0
- package/docs/schedule.md +467 -0
- package/docs/tagged-error-examples.test.ts +494 -0
- package/docs/tagged-error.md +730 -0
- package/docs/visualization-examples.test.ts +663 -0
- package/docs/visualization.md +395 -0
- package/docs/visualize-examples.md +1 -1
- package/package.json +17 -2
package/docs/match.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# Match
|
|
2
|
+
|
|
3
|
+
Exhaustive pattern matching for discriminated unions. Ensures all cases are handled at compile time.
|
|
4
|
+
|
|
5
|
+
## The Problem: Switch Statements Miss Cases
|
|
6
|
+
|
|
7
|
+
TypeScript doesn't catch missing cases in switch statements:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
type Status =
|
|
11
|
+
| { _tag: 'Pending' }
|
|
12
|
+
| { _tag: 'Running'; progress: number }
|
|
13
|
+
| { _tag: 'Completed'; result: string }
|
|
14
|
+
| { _tag: 'Failed'; error: string }
|
|
15
|
+
|
|
16
|
+
// ❌ No compile error when you add a new status!
|
|
17
|
+
function getMessage(status: Status): string {
|
|
18
|
+
switch (status._tag) {
|
|
19
|
+
case 'Pending': return 'Waiting...';
|
|
20
|
+
case 'Running': return `${status.progress}%`;
|
|
21
|
+
case 'Completed': return status.result;
|
|
22
|
+
// Forgot 'Failed' - runtime error!
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The Solution: Exhaustive Matching
|
|
28
|
+
|
|
29
|
+
Use `Match` for compile-time exhaustiveness checking:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { Match } from '@jagreehal/workflow';
|
|
33
|
+
|
|
34
|
+
// ✓ TypeScript error if any case is missing
|
|
35
|
+
function getMessage(status: Status): string {
|
|
36
|
+
return Match.value(status)
|
|
37
|
+
.pipe(Match.tag("Pending", () => "Waiting..."))
|
|
38
|
+
.pipe(Match.tag("Running", s => `${s.progress}%`))
|
|
39
|
+
.pipe(Match.tag("Completed", s => s.result))
|
|
40
|
+
.pipe(Match.tag("Failed", s => `Error: ${s.error}`))
|
|
41
|
+
.pipe(Match.exhaustive);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If you forget a case, TypeScript shows an error at `.pipe(Match.exhaustive)`.
|
|
46
|
+
|
|
47
|
+
## Core API
|
|
48
|
+
|
|
49
|
+
### `Match.value` - Start Matching
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { Match } from '@jagreehal/workflow';
|
|
53
|
+
|
|
54
|
+
type Event =
|
|
55
|
+
| { _tag: 'UserCreated'; userId: string; name: string }
|
|
56
|
+
| { _tag: 'UserUpdated'; userId: string }
|
|
57
|
+
| { _tag: 'UserDeleted'; userId: string }
|
|
58
|
+
|
|
59
|
+
const message = Match.value(event)
|
|
60
|
+
.pipe(Match.tag("UserCreated", e => `Created: ${e.name}`))
|
|
61
|
+
.pipe(Match.tag("UserUpdated", e => `Updated: ${e.userId}`))
|
|
62
|
+
.pipe(Match.tag("UserDeleted", e => `Deleted: ${e.userId}`))
|
|
63
|
+
.pipe(Match.exhaustive);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `Match.tag` - Handle One Case
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
Match.value(status)
|
|
70
|
+
.pipe(Match.tag("Pending", () => "waiting")) // Handle Pending
|
|
71
|
+
.pipe(Match.tag("Running", s => s.progress)) // Handle Running
|
|
72
|
+
.pipe(Match.tag("Completed", s => s.result)) // Handle Completed
|
|
73
|
+
.pipe(Match.tag("Failed", s => s.error)) // Handle Failed
|
|
74
|
+
.pipe(Match.exhaustive);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each `Match.tag` removes that case from the remaining unhandled set. TypeScript tracks this.
|
|
78
|
+
|
|
79
|
+
### `Match.tags` - Handle Multiple Cases
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Handle multiple cases at once
|
|
83
|
+
Match.value(event)
|
|
84
|
+
.pipe(Match.tags({
|
|
85
|
+
UserCreated: e => `Created: ${e.name}`,
|
|
86
|
+
UserUpdated: e => `Updated: ${e.userId}`,
|
|
87
|
+
UserDeleted: e => `Deleted: ${e.userId}`,
|
|
88
|
+
}))
|
|
89
|
+
.pipe(Match.exhaustive);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `Match.exhaustive` - Complete the Match
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// TypeScript error if any case is not handled
|
|
96
|
+
const result = Match.value(status)
|
|
97
|
+
.pipe(Match.tag("Pending", () => 0))
|
|
98
|
+
.pipe(Match.tag("Running", s => s.progress))
|
|
99
|
+
// Missing: Completed, Failed
|
|
100
|
+
.pipe(Match.exhaustive); // ❌ Type error!
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Default Handling
|
|
104
|
+
|
|
105
|
+
### `Match.orElse` - Default Handler
|
|
106
|
+
|
|
107
|
+
Handle specific cases, use a function for everything else:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const message = Match.value(status)
|
|
111
|
+
.pipe(Match.tag("Completed", s => `Done: ${s.result}`))
|
|
112
|
+
.pipe(Match.orElse(s => `Status: ${s._tag}`));
|
|
113
|
+
|
|
114
|
+
// Completed → "Done: success"
|
|
115
|
+
// Pending → "Status: Pending"
|
|
116
|
+
// Running → "Status: Running"
|
|
117
|
+
// Failed → "Status: Failed"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `Match.orElseValue` - Default Value
|
|
121
|
+
|
|
122
|
+
Return a constant for unhandled cases:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const result = Match.value(status)
|
|
126
|
+
.pipe(Match.tag("Completed", s => s.result))
|
|
127
|
+
.pipe(Match.orElseValue("not completed"));
|
|
128
|
+
|
|
129
|
+
// Completed → "success"
|
|
130
|
+
// Everything else → "not completed"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Conditional Matching
|
|
134
|
+
|
|
135
|
+
### `Match.when` - Predicate-Based Matching
|
|
136
|
+
|
|
137
|
+
Add conditions beyond just the tag:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
type User = { _tag: 'User'; name: string; isAdmin: boolean };
|
|
141
|
+
|
|
142
|
+
const greeting = Match.value(user)
|
|
143
|
+
.pipe(Match.tag("User", u => `Hello ${u.name}`)) // Default
|
|
144
|
+
.pipe(Match.when("User", u => u.isAdmin, u => `Hello Admin ${u.name}!`)) // Override for admins
|
|
145
|
+
.pipe(Match.exhaustive);
|
|
146
|
+
|
|
147
|
+
// { _tag: 'User', name: 'Alice', isAdmin: true } → "Hello Admin Alice!"
|
|
148
|
+
// { _tag: 'User', name: 'Bob', isAdmin: false } → "Hello Bob"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Order matters: `when` overrides a previous `tag` handler when the predicate is true.
|
|
152
|
+
|
|
153
|
+
## Type Guards
|
|
154
|
+
|
|
155
|
+
### `Match.is` - Single Tag Check
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
const event: Event = { _tag: 'UserCreated', userId: '1', name: 'Alice' };
|
|
159
|
+
|
|
160
|
+
if (Match.is<Event, "UserCreated">("UserCreated")(event)) {
|
|
161
|
+
// TypeScript knows: event.name exists
|
|
162
|
+
console.log(event.name); // "Alice"
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `Match.isOneOf` - Multiple Tag Check
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const isModification = Match.isOneOf<Event, ("UserCreated" | "UserUpdated")[]>(
|
|
170
|
+
"UserCreated",
|
|
171
|
+
"UserUpdated"
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (isModification(event)) {
|
|
175
|
+
// TypeScript knows: event is UserCreated | UserUpdated
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Real-World Scenarios
|
|
180
|
+
|
|
181
|
+
### Payment Processing: Handle Every Possible Outcome
|
|
182
|
+
|
|
183
|
+
Your checkout API returns different result types. Missing one means silent bugs in production.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { Match } from '@jagreehal/workflow';
|
|
187
|
+
|
|
188
|
+
type PaymentResult =
|
|
189
|
+
| { _tag: 'Success'; transactionId: string; amount: number }
|
|
190
|
+
| { _tag: 'Declined'; reason: string; canRetry: boolean }
|
|
191
|
+
| { _tag: 'Fraud'; riskScore: number }
|
|
192
|
+
| { _tag: 'NetworkError'; provider: string }
|
|
193
|
+
|
|
194
|
+
function handlePayment(result: PaymentResult): { action: string; notify: boolean } {
|
|
195
|
+
return Match.value(result)
|
|
196
|
+
.pipe(Match.tag("Success", r => ({
|
|
197
|
+
action: `Charge ${r.transactionId} complete`,
|
|
198
|
+
notify: false
|
|
199
|
+
})))
|
|
200
|
+
.pipe(Match.tag("Declined", r => ({
|
|
201
|
+
action: r.canRetry ? "Show retry prompt" : "Show alternative payment",
|
|
202
|
+
notify: false
|
|
203
|
+
})))
|
|
204
|
+
.pipe(Match.tag("Fraud", r => ({
|
|
205
|
+
action: `Flag order for review (score: ${r.riskScore})`,
|
|
206
|
+
notify: true // Alert fraud team
|
|
207
|
+
})))
|
|
208
|
+
.pipe(Match.tag("NetworkError", r => ({
|
|
209
|
+
action: `Retry with fallback provider (${r.provider} down)`,
|
|
210
|
+
notify: true // Alert ops
|
|
211
|
+
})))
|
|
212
|
+
.pipe(Match.exhaustive);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Add a new payment status later? TypeScript errors until you handle it.
|
|
217
|
+
|
|
218
|
+
### Subscription Billing: Different Logic per Plan
|
|
219
|
+
|
|
220
|
+
Your billing system charges differently based on plan type. Each plan has unique fields.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { Match } from '@jagreehal/workflow';
|
|
224
|
+
|
|
225
|
+
type Subscription =
|
|
226
|
+
| { _tag: 'Free'; userId: string }
|
|
227
|
+
| { _tag: 'Pro'; userId: string; monthlyRate: number }
|
|
228
|
+
| { _tag: 'Enterprise'; userId: string; contractValue: number; billingContact: string }
|
|
229
|
+
|
|
230
|
+
function calculateInvoice(sub: Subscription): number {
|
|
231
|
+
return Match.value(sub)
|
|
232
|
+
.pipe(Match.tag("Free", () => 0))
|
|
233
|
+
.pipe(Match.tag("Pro", s => s.monthlyRate))
|
|
234
|
+
.pipe(Match.tag("Enterprise", s => s.contractValue / 12))
|
|
235
|
+
.pipe(Match.exhaustive);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getBillingEmail(sub: Subscription): string {
|
|
239
|
+
return Match.value(sub)
|
|
240
|
+
.pipe(Match.tag("Enterprise", s => s.billingContact))
|
|
241
|
+
.pipe(Match.orElse(s => `user-${s.userId}@example.com`));
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Notification Routing: Send to the Right Channel
|
|
246
|
+
|
|
247
|
+
Different event types need different notification channels. Miss one and users don't get notified.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { Match } from '@jagreehal/workflow';
|
|
251
|
+
|
|
252
|
+
type NotificationEvent =
|
|
253
|
+
| { _tag: 'OrderShipped'; orderId: string; trackingUrl: string }
|
|
254
|
+
| { _tag: 'PaymentFailed'; amount: number; retryUrl: string }
|
|
255
|
+
| { _tag: 'AccountLocked'; reason: string }
|
|
256
|
+
| { _tag: 'PasswordChanged' }
|
|
257
|
+
|
|
258
|
+
type Channel = 'email' | 'sms' | 'push' | 'slack';
|
|
259
|
+
|
|
260
|
+
function routeNotification(event: NotificationEvent): { channel: Channel; urgent: boolean } {
|
|
261
|
+
return Match.value(event)
|
|
262
|
+
.pipe(Match.tag("OrderShipped", () => ({ channel: 'push' as const, urgent: false })))
|
|
263
|
+
.pipe(Match.tag("PaymentFailed", () => ({ channel: 'email' as const, urgent: true })))
|
|
264
|
+
.pipe(Match.tag("AccountLocked", () => ({ channel: 'sms' as const, urgent: true })))
|
|
265
|
+
.pipe(Match.tag("PasswordChanged", () => ({ channel: 'email' as const, urgent: false })))
|
|
266
|
+
.pipe(Match.exhaustive);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Form Validation: Collect All Errors with Context
|
|
271
|
+
|
|
272
|
+
Validation returns different error types. You need to show the right message for each.
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { Match } from '@jagreehal/workflow';
|
|
276
|
+
|
|
277
|
+
type ValidationError =
|
|
278
|
+
| { _tag: 'Required'; field: string }
|
|
279
|
+
| { _tag: 'TooShort'; field: string; min: number; actual: number }
|
|
280
|
+
| { _tag: 'InvalidFormat'; field: string; expected: string }
|
|
281
|
+
| { _tag: 'AlreadyExists'; field: string; value: string }
|
|
282
|
+
|
|
283
|
+
function formatValidationError(error: ValidationError): string {
|
|
284
|
+
return Match.value(error)
|
|
285
|
+
.pipe(Match.tag("Required", e => `${e.field} is required`))
|
|
286
|
+
.pipe(Match.tag("TooShort", e => `${e.field} must be at least ${e.min} characters (got ${e.actual})`))
|
|
287
|
+
.pipe(Match.tag("InvalidFormat", e => `${e.field} must be a valid ${e.expected}`))
|
|
288
|
+
.pipe(Match.tag("AlreadyExists", e => `${e.field} "${e.value}" is already taken`))
|
|
289
|
+
.pipe(Match.exhaustive);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## More Examples
|
|
294
|
+
|
|
295
|
+
### Event Handler
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
type DomainEvent =
|
|
299
|
+
| { _tag: 'OrderPlaced'; orderId: string; items: string[] }
|
|
300
|
+
| { _tag: 'OrderShipped'; orderId: string; trackingNumber: string }
|
|
301
|
+
| { _tag: 'OrderDelivered'; orderId: string }
|
|
302
|
+
| { _tag: 'OrderCancelled'; orderId: string; reason: string }
|
|
303
|
+
|
|
304
|
+
function handleEvent(event: DomainEvent): void {
|
|
305
|
+
const message = Match.value(event)
|
|
306
|
+
.pipe(Match.tag("OrderPlaced", e => `Order ${e.orderId} placed with ${e.items.length} items`))
|
|
307
|
+
.pipe(Match.tag("OrderShipped", e => `Order ${e.orderId} shipped: ${e.trackingNumber}`))
|
|
308
|
+
.pipe(Match.tag("OrderDelivered", e => `Order ${e.orderId} delivered`))
|
|
309
|
+
.pipe(Match.tag("OrderCancelled", e => `Order ${e.orderId} cancelled: ${e.reason}`))
|
|
310
|
+
.pipe(Match.exhaustive);
|
|
311
|
+
|
|
312
|
+
console.log(message);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Result Handling
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
type ApiResult =
|
|
320
|
+
| { _tag: 'Success'; data: unknown }
|
|
321
|
+
| { _tag: 'NotFound'; resource: string }
|
|
322
|
+
| { _tag: 'Unauthorized' }
|
|
323
|
+
| { _tag: 'RateLimited'; retryAfter: number }
|
|
324
|
+
|
|
325
|
+
function formatError(result: ApiResult): string | null {
|
|
326
|
+
return Match.value(result)
|
|
327
|
+
.pipe(Match.tag("Success", () => null))
|
|
328
|
+
.pipe(Match.tag("NotFound", r => `Resource not found: ${r.resource}`))
|
|
329
|
+
.pipe(Match.tag("Unauthorized", () => "Please log in"))
|
|
330
|
+
.pipe(Match.tag("RateLimited", r => `Too many requests. Retry in ${r.retryAfter}s`))
|
|
331
|
+
.pipe(Match.exhaustive);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### State Machine
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
type ConnectionState =
|
|
339
|
+
| { _tag: 'Disconnected' }
|
|
340
|
+
| { _tag: 'Connecting'; attempt: number }
|
|
341
|
+
| { _tag: 'Connected'; sessionId: string }
|
|
342
|
+
| { _tag: 'Reconnecting'; attempt: number; lastError: string }
|
|
343
|
+
|
|
344
|
+
function getStatusIcon(state: ConnectionState): string {
|
|
345
|
+
return Match.value(state)
|
|
346
|
+
.pipe(Match.tag("Disconnected", () => "⚫"))
|
|
347
|
+
.pipe(Match.tag("Connecting", () => "🟡"))
|
|
348
|
+
.pipe(Match.tag("Connected", () => "🟢"))
|
|
349
|
+
.pipe(Match.tag("Reconnecting", () => "🟠"))
|
|
350
|
+
.pipe(Match.exhaustive);
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Partial Handling with Defaults
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Only care about certain states
|
|
358
|
+
const isActive = Match.value(state)
|
|
359
|
+
.pipe(Match.tag("Connected", () => true))
|
|
360
|
+
.pipe(Match.tag("Reconnecting", () => true))
|
|
361
|
+
.pipe(Match.orElseValue(false));
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Best Practices
|
|
365
|
+
|
|
366
|
+
### DO: Use for discriminated unions
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// ✓ Good - tagged union
|
|
370
|
+
type Result =
|
|
371
|
+
| { _tag: 'Ok'; value: string }
|
|
372
|
+
| { _tag: 'Err'; error: Error }
|
|
373
|
+
|
|
374
|
+
Match.value(result)
|
|
375
|
+
.pipe(Match.tag("Ok", r => r.value))
|
|
376
|
+
.pipe(Match.tag("Err", r => r.error.message))
|
|
377
|
+
.pipe(Match.exhaustive);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### DON'T: Use for simple boolean/null checks
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// ✗ Overkill - just use if/else
|
|
384
|
+
const message = value ? "yes" : "no";
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### DO: Extract handlers for complex logic
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// ✓ Keep match expressions clean
|
|
391
|
+
const handleCreated = (e: Extract<Event, { _tag: 'UserCreated' }>) => {
|
|
392
|
+
// Complex logic here
|
|
393
|
+
return `Created: ${e.name}`;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
Match.value(event)
|
|
397
|
+
.pipe(Match.tag("UserCreated", handleCreated))
|
|
398
|
+
.pipe(Match.tag("UserUpdated", handleUpdated))
|
|
399
|
+
.pipe(Match.tag("UserDeleted", handleDeleted))
|
|
400
|
+
.pipe(Match.exhaustive);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Summary
|
|
404
|
+
|
|
405
|
+
| Function | Purpose |
|
|
406
|
+
| -------- | ------- |
|
|
407
|
+
| `Match.value(x)` | Start matching on a value |
|
|
408
|
+
| `Match.tag(tag, fn)` | Handle one case |
|
|
409
|
+
| `Match.tags({ ... })` | Handle multiple cases |
|
|
410
|
+
| `Match.when(tag, pred, fn)` | Conditional handling |
|
|
411
|
+
| `Match.exhaustive` | Complete match (all cases required) |
|
|
412
|
+
| `Match.orElse(fn)` | Default handler |
|
|
413
|
+
| `Match.orElseValue(v)` | Default value |
|
|
414
|
+
| `Match.is(tag)` | Type guard for one tag |
|
|
415
|
+
| `Match.isOneOf(...tags)` | Type guard for multiple tags |
|
|
416
|
+
|
|
417
|
+
**The key insight:** Let the compiler catch missing cases. When you add a new variant to a union, TypeScript tells you everywhere that needs updating.
|