@martinffx/atelier 2.0.0 → 3.0.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/CHANGELOG.md +14 -0
- package/README.md +94 -253
- package/package.json +1 -1
- package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +6 -6
- package/skills/spec-brainstorm/SKILL.md +5 -5
- package/skills/spec-implement/SKILL.md +2 -3
- package/skills/spec-plan/SKILL.md +21 -29
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
# Algebraic Data Types (ADTs) in TypeScript
|
|
2
|
-
|
|
3
|
-
Algebraic Data Types enable you to model complex domains precisely by composing types algebraically. TypeScript's union and intersection types provide the foundation for ADTs, allowing you to make illegal states unrepresentable.
|
|
4
|
-
|
|
5
|
-
## What Are ADTs?
|
|
6
|
-
|
|
7
|
-
ADTs come in two flavors:
|
|
8
|
-
|
|
9
|
-
1. **Sum Types (OR)** - A value is one of several variants (tagged unions, discriminated unions)
|
|
10
|
-
2. **Product Types (AND)** - A value contains all fields simultaneously (objects, tuples)
|
|
11
|
-
|
|
12
|
-
TypeScript supports both through union types (`|`) and object/tuple types.
|
|
13
|
-
|
|
14
|
-
## Sum Types (Tagged Unions)
|
|
15
|
-
|
|
16
|
-
A sum type represents "one of several possible variants." Each variant is distinct and mutually exclusive.
|
|
17
|
-
|
|
18
|
-
### Basic Pattern
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
type Shape =
|
|
22
|
-
| { kind: "circle"; radius: number }
|
|
23
|
-
| { kind: "rectangle"; width: number; height: number }
|
|
24
|
-
| { kind: "triangle"; base: number; height: number }
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The `kind` field is the **discriminant** (also called "tag"). It tells TypeScript which variant you have, enabling type narrowing.
|
|
28
|
-
|
|
29
|
-
### Pattern Matching with Exhaustiveness
|
|
30
|
-
|
|
31
|
-
Always use `switch` with `assertNever` to ensure you handle all cases:
|
|
32
|
-
|
|
33
|
-
```typescript
|
|
34
|
-
function area(shape: Shape): number {
|
|
35
|
-
switch (shape.kind) {
|
|
36
|
-
case "circle":
|
|
37
|
-
// TypeScript knows: shape.radius exists
|
|
38
|
-
return Math.PI * shape.radius ** 2
|
|
39
|
-
case "rectangle":
|
|
40
|
-
// TypeScript knows: shape.width and shape.height exist
|
|
41
|
-
return shape.width * shape.height
|
|
42
|
-
case "triangle":
|
|
43
|
-
// TypeScript knows: shape.base and shape.height exist
|
|
44
|
-
return (shape.base * shape.height) / 2
|
|
45
|
-
default:
|
|
46
|
-
// Compiler error if you add a new variant without handling it
|
|
47
|
-
assertNever(shape)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const assertNever = (x: never): never => {
|
|
52
|
-
throw new Error(`Unhandled variant: ${JSON.stringify(x)}`)
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**Why this matters**: If you add a new `Shape` variant (e.g., `pentagon`), TypeScript will produce a compile error in `area()` until you handle it.
|
|
57
|
-
|
|
58
|
-
### Discriminant Field Conventions
|
|
59
|
-
|
|
60
|
-
Choose a consistent discriminant field name across your codebase:
|
|
61
|
-
|
|
62
|
-
- `kind` - Common in domain models
|
|
63
|
-
- `type` - Common in event systems
|
|
64
|
-
- `_tag` - Common in functional programming libraries (fp-ts, effect-ts)
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
// Good: Consistent discriminant
|
|
68
|
-
type PaymentEvent =
|
|
69
|
-
| { kind: "initiated"; amount: Cents }
|
|
70
|
-
| { kind: "authorized"; authCode: string }
|
|
71
|
-
| { kind: "settled"; ledgerId: string }
|
|
72
|
-
|
|
73
|
-
// Avoid: Mixed discriminants
|
|
74
|
-
type Bad =
|
|
75
|
-
| { kind: "a"; data: string }
|
|
76
|
-
| { type: "b"; info: number } // Different field name!
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Product Types (Objects and Tuples)
|
|
80
|
-
|
|
81
|
-
A product type contains multiple fields simultaneously. TypeScript objects and tuples are product types.
|
|
82
|
-
|
|
83
|
-
### Object Product Type
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
type User = {
|
|
87
|
-
id: string
|
|
88
|
-
email: string
|
|
89
|
-
role: "admin" | "user"
|
|
90
|
-
createdAt: Date
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// User has ALL four fields
|
|
94
|
-
const user: User = {
|
|
95
|
-
id: "u123",
|
|
96
|
-
email: "alice@example.com",
|
|
97
|
-
role: "admin",
|
|
98
|
-
createdAt: new Date(),
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Tuple Product Type
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
type Coordinate = [number, number] // x and y
|
|
106
|
-
|
|
107
|
-
const point: Coordinate = [10, 20]
|
|
108
|
-
const [x, y] = point // Destructuring
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Combining Sum and Product Types
|
|
112
|
-
|
|
113
|
-
Real-world domains often combine both:
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
type PaymentMethod =
|
|
117
|
-
| { kind: "card"; last4: string; brand: string; expiryMonth: number; expiryYear: number }
|
|
118
|
-
| { kind: "ach"; accountNumber: string; routingNumber: string; bankName: string }
|
|
119
|
-
| { kind: "wallet"; provider: "apple" | "google"; deviceId: string }
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Each variant (sum type) contains multiple fields (product type).
|
|
123
|
-
|
|
124
|
-
## Real-World Example: Transaction States
|
|
125
|
-
|
|
126
|
-
Model a transaction lifecycle as a state machine:
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
type Millis = number & { __brand: "Millis" }
|
|
130
|
-
type Cents = number & { __brand: "Cents" }
|
|
131
|
-
|
|
132
|
-
type TxnState =
|
|
133
|
-
| { kind: "pending"; createdAt: Millis }
|
|
134
|
-
| { kind: "authorized"; authCode: string; authorizedAt: Millis }
|
|
135
|
-
| { kind: "settled"; ledgerId: string; settledAt: Millis }
|
|
136
|
-
| { kind: "failed"; reason: FailureReason; failedAt: Millis }
|
|
137
|
-
| { kind: "reversed"; originalLedgerId: string; reversedAt: Millis }
|
|
138
|
-
|
|
139
|
-
type FailureReason =
|
|
140
|
-
| { kind: "insufficient_funds" }
|
|
141
|
-
| { kind: "invalid_account" }
|
|
142
|
-
| { kind: "network_error"; retryable: boolean }
|
|
143
|
-
|
|
144
|
-
function canReverse(state: TxnState): boolean {
|
|
145
|
-
switch (state.kind) {
|
|
146
|
-
case "pending":
|
|
147
|
-
return false
|
|
148
|
-
case "authorized":
|
|
149
|
-
return false
|
|
150
|
-
case "settled":
|
|
151
|
-
return true // Only settled transactions can be reversed
|
|
152
|
-
case "failed":
|
|
153
|
-
return false
|
|
154
|
-
case "reversed":
|
|
155
|
-
return false // Already reversed
|
|
156
|
-
default:
|
|
157
|
-
assertNever(state)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function isTerminal(state: TxnState): boolean {
|
|
162
|
-
switch (state.kind) {
|
|
163
|
-
case "pending":
|
|
164
|
-
return false
|
|
165
|
-
case "authorized":
|
|
166
|
-
return false
|
|
167
|
-
case "settled":
|
|
168
|
-
return true
|
|
169
|
-
case "failed":
|
|
170
|
-
return true
|
|
171
|
-
case "reversed":
|
|
172
|
-
return true
|
|
173
|
-
default:
|
|
174
|
-
assertNever(state)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Benefits:**
|
|
180
|
-
- Impossible states (e.g., "pending transaction with ledgerId") cannot be represented
|
|
181
|
-
- State transitions are explicit in code
|
|
182
|
-
- Adding new states forces you to update all pattern matches
|
|
183
|
-
|
|
184
|
-
## Real-World Example: Payment Methods
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
type PaymentMethod =
|
|
188
|
-
| { kind: "card"; last4: string; brand: string }
|
|
189
|
-
| { kind: "ach"; accountNumber: string; routingNumber: string }
|
|
190
|
-
| { kind: "wallet"; provider: "apple" | "google" }
|
|
191
|
-
|
|
192
|
-
function formatPaymentMethod(method: PaymentMethod): string {
|
|
193
|
-
switch (method.kind) {
|
|
194
|
-
case "card":
|
|
195
|
-
return `${method.brand} ending in ${method.last4}`
|
|
196
|
-
case "ach":
|
|
197
|
-
return `Bank account ending in ${method.accountNumber.slice(-4)}`
|
|
198
|
-
case "wallet":
|
|
199
|
-
return `${method.provider} Pay`
|
|
200
|
-
default:
|
|
201
|
-
assertNever(method)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function isCardPayment(method: PaymentMethod): boolean {
|
|
206
|
-
return method.kind === "card"
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
## Real-World Example: Call Session States
|
|
211
|
-
|
|
212
|
-
Model telephony call session states:
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
type CallSession =
|
|
216
|
-
| { state: "idle" }
|
|
217
|
-
| { state: "ringing"; callerId: string; startedAt: Millis }
|
|
218
|
-
| { state: "active"; callerId: string; answeredAt: Millis }
|
|
219
|
-
| { state: "hold"; callerId: string; heldAt: Millis }
|
|
220
|
-
| { state: "ended"; callerId: string; duration: Millis; endReason: EndReason }
|
|
221
|
-
|
|
222
|
-
type EndReason =
|
|
223
|
-
| { kind: "hangup"; initiator: "caller" | "callee" }
|
|
224
|
-
| { kind: "timeout" }
|
|
225
|
-
| { kind: "error"; code: string }
|
|
226
|
-
|
|
227
|
-
function canTransferCall(session: CallSession): boolean {
|
|
228
|
-
switch (session.state) {
|
|
229
|
-
case "idle":
|
|
230
|
-
return false
|
|
231
|
-
case "ringing":
|
|
232
|
-
return false
|
|
233
|
-
case "active":
|
|
234
|
-
return true // Only active calls can be transferred
|
|
235
|
-
case "hold":
|
|
236
|
-
return true // Calls on hold can be transferred
|
|
237
|
-
case "ended":
|
|
238
|
-
return false
|
|
239
|
-
default:
|
|
240
|
-
assertNever(session)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
## Type Narrowing Techniques
|
|
246
|
-
|
|
247
|
-
TypeScript provides several ways to narrow sum types:
|
|
248
|
-
|
|
249
|
-
### 1. Switch Statement (Recommended)
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
switch (shape.kind) {
|
|
253
|
-
case "circle":
|
|
254
|
-
return Math.PI * shape.radius ** 2
|
|
255
|
-
// ... other cases
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### 2. If-Else Chain
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
if (shape.kind === "circle") {
|
|
263
|
-
return Math.PI * shape.radius ** 2
|
|
264
|
-
} else if (shape.kind === "rectangle") {
|
|
265
|
-
return shape.width * shape.height
|
|
266
|
-
} else if (shape.kind === "triangle") {
|
|
267
|
-
return (shape.base * shape.height) / 2
|
|
268
|
-
} else {
|
|
269
|
-
assertNever(shape)
|
|
270
|
-
}
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### 3. Early Return Pattern
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
function area(shape: Shape): number {
|
|
277
|
-
if (shape.kind === "circle") {
|
|
278
|
-
return Math.PI * shape.radius ** 2
|
|
279
|
-
}
|
|
280
|
-
if (shape.kind === "rectangle") {
|
|
281
|
-
return shape.width * shape.height
|
|
282
|
-
}
|
|
283
|
-
if (shape.kind === "triangle") {
|
|
284
|
-
return (shape.base * shape.height) / 2
|
|
285
|
-
}
|
|
286
|
-
assertNever(shape)
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### 4. Type Guards
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
function isCircle(shape: Shape): shape is Extract<Shape, { kind: "circle" }> {
|
|
294
|
-
return shape.kind === "circle"
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (isCircle(shape)) {
|
|
298
|
-
console.log(shape.radius) // Type-safe
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
## Nested Sum Types
|
|
303
|
-
|
|
304
|
-
Sum types can be nested for complex domains:
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
type ApiResponse<T> =
|
|
308
|
-
| { status: "success"; data: T }
|
|
309
|
-
| { status: "error"; error: ApiError }
|
|
310
|
-
|
|
311
|
-
type ApiError =
|
|
312
|
-
| { kind: "validation"; fields: Record<string, string[]> }
|
|
313
|
-
| { kind: "auth"; message: string }
|
|
314
|
-
| { kind: "network"; retryable: boolean }
|
|
315
|
-
| { kind: "server"; statusCode: number }
|
|
316
|
-
|
|
317
|
-
function handleResponse<T>(response: ApiResponse<T>): void {
|
|
318
|
-
switch (response.status) {
|
|
319
|
-
case "success":
|
|
320
|
-
console.log(response.data)
|
|
321
|
-
break
|
|
322
|
-
case "error":
|
|
323
|
-
switch (response.error.kind) {
|
|
324
|
-
case "validation":
|
|
325
|
-
console.error("Validation errors:", response.error.fields)
|
|
326
|
-
break
|
|
327
|
-
case "auth":
|
|
328
|
-
console.error("Auth error:", response.error.message)
|
|
329
|
-
break
|
|
330
|
-
case "network":
|
|
331
|
-
if (response.error.retryable) {
|
|
332
|
-
console.log("Retrying...")
|
|
333
|
-
}
|
|
334
|
-
break
|
|
335
|
-
case "server":
|
|
336
|
-
console.error("Server error:", response.error.statusCode)
|
|
337
|
-
break
|
|
338
|
-
default:
|
|
339
|
-
assertNever(response.error)
|
|
340
|
-
}
|
|
341
|
-
break
|
|
342
|
-
default:
|
|
343
|
-
assertNever(response)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
## Generic Sum Types
|
|
349
|
-
|
|
350
|
-
Use generics to create reusable sum type patterns:
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
type RemoteData<T, E> =
|
|
354
|
-
| { status: "not-asked" }
|
|
355
|
-
| { status: "loading" }
|
|
356
|
-
| { status: "success"; data: T }
|
|
357
|
-
| { status: "failure"; error: E }
|
|
358
|
-
|
|
359
|
-
// Usage
|
|
360
|
-
type UserProfile = RemoteData<User, string>
|
|
361
|
-
|
|
362
|
-
function renderProfile(profile: UserProfile): string {
|
|
363
|
-
switch (profile.status) {
|
|
364
|
-
case "not-asked":
|
|
365
|
-
return "Click to load"
|
|
366
|
-
case "loading":
|
|
367
|
-
return "Loading..."
|
|
368
|
-
case "success":
|
|
369
|
-
return `Welcome, ${profile.data.email}`
|
|
370
|
-
case "failure":
|
|
371
|
-
return `Error: ${profile.error}`
|
|
372
|
-
default:
|
|
373
|
-
assertNever(profile)
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
## Common Patterns
|
|
379
|
-
|
|
380
|
-
### Boolean Elimination
|
|
381
|
-
|
|
382
|
-
Replace boolean flags with explicit states:
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
// Bad: Boolean soup
|
|
386
|
-
type Order = {
|
|
387
|
-
id: string
|
|
388
|
-
isPaid: boolean
|
|
389
|
-
isShipped: boolean
|
|
390
|
-
isCancelled: boolean
|
|
391
|
-
}
|
|
392
|
-
// What does { isPaid: true, isCancelled: true } mean?
|
|
393
|
-
|
|
394
|
-
// Good: Explicit states
|
|
395
|
-
type OrderStatus =
|
|
396
|
-
| { kind: "pending" }
|
|
397
|
-
| { kind: "paid"; paidAt: Date }
|
|
398
|
-
| { kind: "shipped"; trackingNumber: string; shippedAt: Date }
|
|
399
|
-
| { kind: "delivered"; deliveredAt: Date }
|
|
400
|
-
| { kind: "cancelled"; reason: string; cancelledAt: Date }
|
|
401
|
-
|
|
402
|
-
type Order = {
|
|
403
|
-
id: string
|
|
404
|
-
status: OrderStatus
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
### State Transitions
|
|
409
|
-
|
|
410
|
-
Encode valid state transitions in types:
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
type TxnState =
|
|
414
|
-
| { kind: "pending" }
|
|
415
|
-
| { kind: "settled"; ledgerId: string }
|
|
416
|
-
| { kind: "failed"; reason: string }
|
|
417
|
-
|
|
418
|
-
function settle(state: TxnState, ledgerId: string): TxnState {
|
|
419
|
-
if (state.kind !== "pending") {
|
|
420
|
-
throw new Error("Can only settle pending transactions")
|
|
421
|
-
}
|
|
422
|
-
return { kind: "settled", ledgerId }
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function fail(state: TxnState, reason: string): TxnState {
|
|
426
|
-
if (state.kind !== "pending") {
|
|
427
|
-
throw new Error("Can only fail pending transactions")
|
|
428
|
-
}
|
|
429
|
-
return { kind: "failed", reason }
|
|
430
|
-
}
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
## Best Practices
|
|
434
|
-
|
|
435
|
-
1. **Use consistent discriminant names** (`kind`, `type`, or `_tag`)
|
|
436
|
-
2. **Always include `assertNever` in default case** for exhaustiveness
|
|
437
|
-
3. **Prefer sum types over boolean flags** for state modeling
|
|
438
|
-
4. **Make illegal states unrepresentable** through type design
|
|
439
|
-
5. **Use nested sum types** for complex error hierarchies
|
|
440
|
-
6. **Document state transitions** in comments or type definitions
|
|
441
|
-
7. **Leverage generics** for reusable patterns (RemoteData, Result, etc.)
|
|
442
|
-
|
|
443
|
-
## Testing ADTs
|
|
444
|
-
|
|
445
|
-
Test each variant and all edge cases:
|
|
446
|
-
|
|
447
|
-
```typescript
|
|
448
|
-
describe("area", () => {
|
|
449
|
-
it("calculates circle area", () => {
|
|
450
|
-
const circle: Shape = { kind: "circle", radius: 10 }
|
|
451
|
-
expect(area(circle)).toBeCloseTo(314.159)
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
it("calculates rectangle area", () => {
|
|
455
|
-
const rect: Shape = { kind: "rectangle", width: 5, height: 10 }
|
|
456
|
-
expect(area(rect)).toBe(50)
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
it("calculates triangle area", () => {
|
|
460
|
-
const triangle: Shape = { kind: "triangle", base: 10, height: 5 }
|
|
461
|
-
expect(area(triangle)).toBe(25)
|
|
462
|
-
})
|
|
463
|
-
})
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
## Migration from Booleans
|
|
467
|
-
|
|
468
|
-
Before:
|
|
469
|
-
```typescript
|
|
470
|
-
type User = {
|
|
471
|
-
id: string
|
|
472
|
-
isActive: boolean
|
|
473
|
-
isBanned: boolean
|
|
474
|
-
isEmailVerified: boolean
|
|
475
|
-
}
|
|
476
|
-
// What does { isActive: true, isBanned: true } mean?
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
After:
|
|
480
|
-
```typescript
|
|
481
|
-
type UserStatus =
|
|
482
|
-
| { kind: "pending-verification"; email: string }
|
|
483
|
-
| { kind: "active" }
|
|
484
|
-
| { kind: "banned"; reason: string; bannedAt: Date }
|
|
485
|
-
| { kind: "deactivated"; deactivatedAt: Date }
|
|
486
|
-
|
|
487
|
-
type User = {
|
|
488
|
-
id: string
|
|
489
|
-
status: UserStatus
|
|
490
|
-
}
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
## Further Reading
|
|
494
|
-
|
|
495
|
-
- [Option & Result](./option-result.md) - Type-safe error handling patterns
|
|
496
|
-
- [Branded Types](./branded-types.md) - Smart constructors and nominal typing
|
|
497
|
-
- [Migration Guide](./migration-guide.md) - Step-by-step adoption playbook
|