@jarrodmedrano/claude-skills 1.0.3 → 1.0.5
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/.claude/skills/bevy/SKILL.md +406 -0
- package/.claude/skills/bevy/references/bevy_specific_tips.md +385 -0
- package/.claude/skills/bevy/references/common_pitfalls.md +217 -0
- package/.claude/skills/bevy/references/ecs_patterns.md +277 -0
- package/.claude/skills/bevy/references/project_structure.md +116 -0
- package/.claude/skills/bevy/references/ui_development.md +147 -0
- package/.claude/skills/domain-driven-design/SKILL.md +459 -0
- package/.claude/skills/domain-driven-design/references/ddd_foundations_and_patterns.md +664 -0
- package/.claude/skills/domain-driven-design/references/rich_hickey_principles.md +406 -0
- package/.claude/skills/domain-driven-design/references/visualization_examples.md +790 -0
- package/.claude/skills/domain-driven-design/references/wlaschin_patterns.md +639 -0
- package/.claude/skills/godot/SKILL.md +728 -0
- package/.claude/skills/godot/assets/templates/attribute_template.gd +109 -0
- package/.claude/skills/godot/assets/templates/component_template.gd +76 -0
- package/.claude/skills/godot/assets/templates/interaction_template.gd +108 -0
- package/.claude/skills/godot/assets/templates/item_resource.tres +11 -0
- package/.claude/skills/godot/assets/templates/spell_resource.tres +20 -0
- package/.claude/skills/godot/references/architecture-patterns.md +608 -0
- package/.claude/skills/godot/references/common-pitfalls.md +518 -0
- package/.claude/skills/godot/references/file-formats.md +491 -0
- package/.claude/skills/godot/references/godot4-physics-api.md +302 -0
- package/.claude/skills/godot/scripts/validate_tres.py +145 -0
- package/.claude/skills/godot/scripts/validate_tscn.py +170 -0
- package/.claude/skills/guitar-fretboard-mastery/SKILL.md +179 -0
- package/.claude/skills/guitar-fretboard-mastery/guitar-fretboard-mastery.skill +0 -0
- package/.claude/skills/react-three-fiber/SKILL.md +2055 -0
- package/.claude/skills/react-three-fiber/scripts/build-scene.ts +171 -0
- package/package.json +1 -1
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
# Scott Wlaschin's Type-Driven Design Patterns
|
|
2
|
+
|
|
3
|
+
This reference compiles patterns and principles from Scott Wlaschin's work on domain modeling, functional architecture, and type-driven design.
|
|
4
|
+
|
|
5
|
+
## Core Philosophy
|
|
6
|
+
|
|
7
|
+
**"Make Illegal States Unrepresentable"**
|
|
8
|
+
|
|
9
|
+
Use the type system to eliminate entire categories of bugs at compile time. If a state shouldn't exist in your domain, make it impossible to construct.
|
|
10
|
+
|
|
11
|
+
## Domain Modeling Made Functional
|
|
12
|
+
|
|
13
|
+
### Understanding the Domain
|
|
14
|
+
|
|
15
|
+
**Key Questions:**
|
|
16
|
+
1. What are the inputs and outputs?
|
|
17
|
+
2. What can happen? (scenarios, workflows)
|
|
18
|
+
3. What can go wrong? (errors, edge cases)
|
|
19
|
+
4. What are the business rules and constraints?
|
|
20
|
+
5. What are the invariants that must always hold?
|
|
21
|
+
|
|
22
|
+
**Process:**
|
|
23
|
+
1. Talk to domain experts using their language
|
|
24
|
+
2. Document workflows as transformations
|
|
25
|
+
3. Identify the things (nouns) and actions (verbs)
|
|
26
|
+
4. Model the lifecycle and state transitions
|
|
27
|
+
5. Capture rules and constraints as types
|
|
28
|
+
|
|
29
|
+
### Workflows as Pipelines
|
|
30
|
+
|
|
31
|
+
Model business workflows as data transformation pipelines:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Input → Validate → Execute Business Logic → Persist → Output
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each step:
|
|
38
|
+
- Takes data as input
|
|
39
|
+
- Performs a transformation
|
|
40
|
+
- Produces data as output
|
|
41
|
+
- May fail (use Result types)
|
|
42
|
+
|
|
43
|
+
**Benefits:**
|
|
44
|
+
- Clear separation of concerns
|
|
45
|
+
- Easy to test each step
|
|
46
|
+
- Easy to compose steps
|
|
47
|
+
- Makes the happy path obvious
|
|
48
|
+
|
|
49
|
+
### Example: Order Placement Workflow
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
UnvalidatedOrder
|
|
53
|
+
→ ValidateOrder
|
|
54
|
+
→ ValidatedOrder
|
|
55
|
+
→ PriceOrder
|
|
56
|
+
→ PricedOrder
|
|
57
|
+
→ PlaceOrder
|
|
58
|
+
→ PlacedOrderEvent
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each arrow is a function. Each type in between represents a distinct state with its own invariants.
|
|
62
|
+
|
|
63
|
+
## Type-Driven Design
|
|
64
|
+
|
|
65
|
+
### Making Illegal States Unrepresentable
|
|
66
|
+
|
|
67
|
+
**Problem:** Optional fields that create invalid combinations
|
|
68
|
+
|
|
69
|
+
**Bad:**
|
|
70
|
+
```typescript
|
|
71
|
+
type Order = {
|
|
72
|
+
id: string;
|
|
73
|
+
// Both can be null, or both can be set - illegal!
|
|
74
|
+
approvedAt: Date | null;
|
|
75
|
+
rejectedAt: Date | null;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Good:**
|
|
80
|
+
```typescript
|
|
81
|
+
type Order = {
|
|
82
|
+
id: OrderId;
|
|
83
|
+
status:
|
|
84
|
+
| { type: "Pending" }
|
|
85
|
+
| { type: "Approved"; approvedAt: Date }
|
|
86
|
+
| { type: "Rejected"; rejectedAt: Date };
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Now impossible to be both approved and rejected, or to have dates without corresponding status.
|
|
91
|
+
|
|
92
|
+
### Constrained Types
|
|
93
|
+
|
|
94
|
+
Create types that can only hold valid values:
|
|
95
|
+
|
|
96
|
+
**Bad:** Primitive obsession
|
|
97
|
+
```typescript
|
|
98
|
+
function createUser(email: string, age: number) { ... }
|
|
99
|
+
// Can pass invalid values: createUser("not-an-email", -5)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Good:** Constrained types
|
|
103
|
+
```typescript
|
|
104
|
+
type EmailAddress = EmailAddress & { __brand: "EmailAddress" };
|
|
105
|
+
type Age = Age & { __brand: "Age" };
|
|
106
|
+
|
|
107
|
+
function createEmailAddress(s: string): Result<EmailAddress, ValidationError> {
|
|
108
|
+
if (isValidEmail(s)) return Ok(s as EmailAddress);
|
|
109
|
+
return Err({ error: "Invalid email format" });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createAge(n: number): Result<Age, ValidationError> {
|
|
113
|
+
if (n >= 0 && n <= 150) return Ok(n as Age);
|
|
114
|
+
return Err({ error: "Age must be between 0 and 150" });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createUser(email: EmailAddress, age: Age) { ... }
|
|
118
|
+
// Can only pass validated values!
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Single Case Unions (Wrapper Types)
|
|
122
|
+
|
|
123
|
+
Use wrapper types to give semantic meaning to primitives:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
type CustomerId = { readonly value: string };
|
|
127
|
+
type ProductId = { readonly value: string };
|
|
128
|
+
type OrderId = { readonly value: string };
|
|
129
|
+
|
|
130
|
+
// Now cannot confuse these:
|
|
131
|
+
function getCustomer(id: CustomerId): Customer { ... }
|
|
132
|
+
// getCustomer(productId) // Type error!
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Benefits:**
|
|
136
|
+
- Type safety
|
|
137
|
+
- Self-documenting code
|
|
138
|
+
- Cannot confuse similar primitives
|
|
139
|
+
- Compiler catches errors
|
|
140
|
+
|
|
141
|
+
### Exhaustive Pattern Matching
|
|
142
|
+
|
|
143
|
+
Use discriminated unions and let the compiler ensure you handle all cases:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
type PaymentMethod =
|
|
147
|
+
| { type: "CreditCard"; cardNumber: string; cvv: string }
|
|
148
|
+
| { type: "PayPal"; email: EmailAddress }
|
|
149
|
+
| { type: "BankTransfer"; accountNumber: string; routingNumber: string };
|
|
150
|
+
|
|
151
|
+
function processPayment(method: PaymentMethod): Result<Receipt, PaymentError> {
|
|
152
|
+
switch (method.type) {
|
|
153
|
+
case "CreditCard":
|
|
154
|
+
return processCreditCard(method.cardNumber, method.cvv);
|
|
155
|
+
case "PayPal":
|
|
156
|
+
return processPayPal(method.email);
|
|
157
|
+
case "BankTransfer":
|
|
158
|
+
return processBankTransfer(method.accountNumber, method.routingNumber);
|
|
159
|
+
// If we add a new payment method, compiler will error here
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### States and Transitions
|
|
165
|
+
|
|
166
|
+
Model entity states explicitly, not as flags:
|
|
167
|
+
|
|
168
|
+
**Bad:**
|
|
169
|
+
```typescript
|
|
170
|
+
type Order = {
|
|
171
|
+
isPaid: boolean;
|
|
172
|
+
isShipped: boolean;
|
|
173
|
+
isCancelled: boolean;
|
|
174
|
+
// Can be paid and cancelled? Shipped but not paid?
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Good:**
|
|
179
|
+
```typescript
|
|
180
|
+
type Order =
|
|
181
|
+
| { state: "Unpaid"; items: OrderLine[] }
|
|
182
|
+
| { state: "Paid"; items: OrderLine[]; paidAt: Date; paymentMethod: PaymentMethod }
|
|
183
|
+
| { state: "Shipped"; items: OrderLine[]; paidAt: Date; shippedAt: Date; trackingNumber: string }
|
|
184
|
+
| { state: "Cancelled"; reason: string };
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Now impossible to be in multiple states or to be shipped without being paid.
|
|
188
|
+
|
|
189
|
+
## Railway-Oriented Programming
|
|
190
|
+
|
|
191
|
+
### The Problem
|
|
192
|
+
|
|
193
|
+
Functions that can fail complicate the happy path:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
function placeOrder(unvalidatedOrder: UnvalidatedOrder) {
|
|
197
|
+
const validatedOrder = validateOrder(unvalidatedOrder);
|
|
198
|
+
if (validatedOrder.isError) return validatedOrder.error;
|
|
199
|
+
|
|
200
|
+
const pricedOrder = priceOrder(validatedOrder.value);
|
|
201
|
+
if (pricedOrder.isError) return pricedOrder.error;
|
|
202
|
+
|
|
203
|
+
const placedOrder = saveOrder(pricedOrder.value);
|
|
204
|
+
if (placedOrder.isError) return placedOrder.error;
|
|
205
|
+
|
|
206
|
+
return placedOrder.value;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Problem:** Error handling obscures the happy path.
|
|
211
|
+
|
|
212
|
+
### Result Type
|
|
213
|
+
|
|
214
|
+
Model success and failure explicitly:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
type Result<T, E> =
|
|
218
|
+
| { ok: true; value: T }
|
|
219
|
+
| { ok: false; error: E };
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Chaining with bind/flatMap
|
|
223
|
+
|
|
224
|
+
Chain operations that return Results:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
function placeOrder(unvalidatedOrder: UnvalidatedOrder): Result<PlacedOrder, OrderError> {
|
|
228
|
+
return validateOrder(unvalidatedOrder)
|
|
229
|
+
.flatMap(priceOrder)
|
|
230
|
+
.flatMap(saveOrder);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**The Railway Metaphor:**
|
|
235
|
+
- Two tracks: Success and Failure
|
|
236
|
+
- Functions switch from success to failure track on error
|
|
237
|
+
- Once on failure track, stay on failure track
|
|
238
|
+
- Clear separation: happy path is just composition
|
|
239
|
+
|
|
240
|
+
### Combining Results
|
|
241
|
+
|
|
242
|
+
When you need multiple independent validations:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
function createUser(
|
|
246
|
+
emailStr: string,
|
|
247
|
+
ageNum: number,
|
|
248
|
+
nameStr: string
|
|
249
|
+
): Result<User, ValidationErrors> {
|
|
250
|
+
const email = createEmail(emailStr);
|
|
251
|
+
const age = createAge(ageNum);
|
|
252
|
+
const name = createName(nameStr);
|
|
253
|
+
|
|
254
|
+
// Collect all errors, not just first
|
|
255
|
+
return combineResults([email, age, name], (email, age, name) => ({
|
|
256
|
+
email,
|
|
257
|
+
age,
|
|
258
|
+
name
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Handling Errors at Boundaries
|
|
264
|
+
|
|
265
|
+
Keep the domain pure; handle effects at edges:
|
|
266
|
+
|
|
267
|
+
**Inside domain (pure):**
|
|
268
|
+
```typescript
|
|
269
|
+
function validateOrder(order: UnvalidatedOrder): Result<ValidatedOrder, ValidationError> {
|
|
270
|
+
// Pure validation logic, no IO
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**At boundary (effects):**
|
|
275
|
+
```typescript
|
|
276
|
+
async function handleOrderRequest(req: Request): Promise<Response> {
|
|
277
|
+
const result = validateOrder(req.body)
|
|
278
|
+
.flatMap(priceOrder)
|
|
279
|
+
.flatMap(saveOrder); // IO happens here
|
|
280
|
+
|
|
281
|
+
if (result.ok) {
|
|
282
|
+
return { status: 200, body: result.value };
|
|
283
|
+
} else {
|
|
284
|
+
return { status: 400, body: { error: result.error } };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Designing with Types
|
|
290
|
+
|
|
291
|
+
### Start with the Types
|
|
292
|
+
|
|
293
|
+
Before writing any implementation:
|
|
294
|
+
|
|
295
|
+
1. **Define the types** that represent domain concepts
|
|
296
|
+
2. **Define the function signatures** (inputs and outputs)
|
|
297
|
+
3. **Implement the functions** (fill in the logic)
|
|
298
|
+
|
|
299
|
+
**Benefits:**
|
|
300
|
+
- Types guide implementation
|
|
301
|
+
- Types serve as documentation
|
|
302
|
+
- Types catch mismatches early
|
|
303
|
+
- Refactoring is safer
|
|
304
|
+
|
|
305
|
+
### Example: Designing a Discount System
|
|
306
|
+
|
|
307
|
+
**Step 1: Define domain types**
|
|
308
|
+
```typescript
|
|
309
|
+
type Product = { id: ProductId; price: Money; category: Category };
|
|
310
|
+
type Customer = { id: CustomerId; membershipLevel: MembershipLevel };
|
|
311
|
+
type Category = "Electronics" | "Clothing" | "Books";
|
|
312
|
+
type MembershipLevel = "Bronze" | "Silver" | "Gold";
|
|
313
|
+
|
|
314
|
+
type DiscountRule =
|
|
315
|
+
| { type: "Percentage"; percentage: number }
|
|
316
|
+
| { type: "FixedAmount"; amount: Money }
|
|
317
|
+
| { type: "BuyXGetY"; buyQuantity: number; getQuantity: number };
|
|
318
|
+
|
|
319
|
+
type Discount = {
|
|
320
|
+
rule: DiscountRule;
|
|
321
|
+
applicableTo: Category[];
|
|
322
|
+
minimumMembershipLevel: MembershipLevel | null;
|
|
323
|
+
};
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Step 2: Define function signatures**
|
|
327
|
+
```typescript
|
|
328
|
+
function calculateDiscount(
|
|
329
|
+
product: Product,
|
|
330
|
+
quantity: number,
|
|
331
|
+
customer: Customer,
|
|
332
|
+
discounts: Discount[]
|
|
333
|
+
): Money;
|
|
334
|
+
|
|
335
|
+
function findApplicableDiscounts(
|
|
336
|
+
product: Product,
|
|
337
|
+
customer: Customer,
|
|
338
|
+
discounts: Discount[]
|
|
339
|
+
): Discount[];
|
|
340
|
+
|
|
341
|
+
function applyDiscount(
|
|
342
|
+
price: Money,
|
|
343
|
+
quantity: number,
|
|
344
|
+
discount: Discount
|
|
345
|
+
): Money;
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Step 3: Implement** (signatures guide what's needed)
|
|
349
|
+
|
|
350
|
+
### Use Types to Model Business Rules
|
|
351
|
+
|
|
352
|
+
Encode business rules in types:
|
|
353
|
+
|
|
354
|
+
**Rule:** "An order must have at least one item"
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
type NonEmptyList<T> = {
|
|
358
|
+
head: T;
|
|
359
|
+
tail: T[];
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
type Order = {
|
|
363
|
+
id: OrderId;
|
|
364
|
+
items: NonEmptyList<OrderLine>; // Cannot be empty!
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Rule:** "Refunds require a reason if amount exceeds $100"
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
type Refund =
|
|
372
|
+
| { amount: Money; reason: null } // reason not needed
|
|
373
|
+
| { amount: Money; reason: string }; // reason required
|
|
374
|
+
|
|
375
|
+
// Factory function enforces rule:
|
|
376
|
+
function createRefund(amount: Money, reason: string | null): Refund {
|
|
377
|
+
if (amount.value > 100 && reason === null) {
|
|
378
|
+
throw new Error("Refund over $100 requires a reason");
|
|
379
|
+
}
|
|
380
|
+
return { amount, reason };
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Functional Architecture Patterns
|
|
385
|
+
|
|
386
|
+
### Onion/Hexagonal Architecture
|
|
387
|
+
|
|
388
|
+
**Layers (inside-out):**
|
|
389
|
+
1. **Domain** - Pure business logic, no dependencies
|
|
390
|
+
2. **Application** - Workflows, orchestration, still pure
|
|
391
|
+
3. **Infrastructure** - IO, databases, external services
|
|
392
|
+
|
|
393
|
+
**Dependency Rule:** Outer layers depend on inner layers, never reverse.
|
|
394
|
+
|
|
395
|
+
**Domain Layer:**
|
|
396
|
+
- Pure functions
|
|
397
|
+
- Domain types
|
|
398
|
+
- Business rules
|
|
399
|
+
- No IO, no frameworks
|
|
400
|
+
|
|
401
|
+
**Application Layer:**
|
|
402
|
+
- Composes domain functions into workflows
|
|
403
|
+
- Still mostly pure
|
|
404
|
+
- Defines interfaces (ports) for infrastructure
|
|
405
|
+
|
|
406
|
+
**Infrastructure Layer:**
|
|
407
|
+
- Implements ports (adapters)
|
|
408
|
+
- Handles IO (database, HTTP, file system)
|
|
409
|
+
- Deals with frameworks and libraries
|
|
410
|
+
|
|
411
|
+
### Dependency Injection via Function Parameters
|
|
412
|
+
|
|
413
|
+
Pass dependencies as function parameters:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Domain function defines what it needs
|
|
417
|
+
function placeOrder(
|
|
418
|
+
validateAddress: (address: Address) => Result<ValidatedAddress, ValidationError>,
|
|
419
|
+
checkInventory: (productId: ProductId) => Promise<boolean>,
|
|
420
|
+
saveOrder: (order: Order) => Promise<Result<void, DbError>>,
|
|
421
|
+
order: UnvalidatedOrder
|
|
422
|
+
): Promise<Result<OrderPlaced, OrderError>> {
|
|
423
|
+
// Implementation uses provided functions
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// At composition root, provide implementations:
|
|
427
|
+
const result = await placeOrder(
|
|
428
|
+
addressValidator.validate,
|
|
429
|
+
inventory.check,
|
|
430
|
+
orderRepository.save,
|
|
431
|
+
incomingOrder
|
|
432
|
+
);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Benefits:**
|
|
436
|
+
- No hidden dependencies
|
|
437
|
+
- Easy to test (pass mock functions)
|
|
438
|
+
- Explicit about requirements
|
|
439
|
+
- No magic or DI container
|
|
440
|
+
|
|
441
|
+
### Command/Query Separation
|
|
442
|
+
|
|
443
|
+
**Commands:** Change state, return void or Result<void, Error>
|
|
444
|
+
- `placeOrder(order): Result<void, OrderError>`
|
|
445
|
+
- `cancelSubscription(id): Result<void, CancellationError>`
|
|
446
|
+
- `updateProfile(profile): Result<void, ValidationError>`
|
|
447
|
+
|
|
448
|
+
**Queries:** Read state, return data, never change state
|
|
449
|
+
- `getOrder(id): Option<Order>`
|
|
450
|
+
- `findCustomersByEmail(email): Customer[]`
|
|
451
|
+
- `getTotalRevenue(): Money`
|
|
452
|
+
|
|
453
|
+
**Benefits:**
|
|
454
|
+
- Clear separation of reads and writes
|
|
455
|
+
- Easier to optimize (cache queries)
|
|
456
|
+
- Easier to reason about (commands have effects, queries don't)
|
|
457
|
+
|
|
458
|
+
### Event Sourcing Pattern
|
|
459
|
+
|
|
460
|
+
Instead of storing current state, store sequence of events:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
type OrderEvent =
|
|
464
|
+
| { type: "OrderPlaced"; orderId: OrderId; items: OrderLine[]; at: Timestamp }
|
|
465
|
+
| { type: "OrderPaid"; orderId: OrderId; amount: Money; at: Timestamp }
|
|
466
|
+
| { type: "OrderShipped"; orderId: OrderId; trackingNumber: string; at: Timestamp }
|
|
467
|
+
| { type: "OrderCancelled"; orderId: OrderId; reason: string; at: Timestamp };
|
|
468
|
+
|
|
469
|
+
function applyEvent(state: Order | null, event: OrderEvent): Order {
|
|
470
|
+
switch (event.type) {
|
|
471
|
+
case "OrderPlaced":
|
|
472
|
+
return { id: event.orderId, items: event.items, status: "Placed" };
|
|
473
|
+
case "OrderPaid":
|
|
474
|
+
return { ...state!, status: "Paid" };
|
|
475
|
+
case "OrderShipped":
|
|
476
|
+
return { ...state!, status: "Shipped", trackingNumber: event.trackingNumber };
|
|
477
|
+
case "OrderCancelled":
|
|
478
|
+
return { ...state!, status: "Cancelled", reason: event.reason };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function reconstruct(events: OrderEvent[]): Order {
|
|
483
|
+
return events.reduce(applyEvent, null)!;
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Benefits:**
|
|
488
|
+
- Complete history
|
|
489
|
+
- Audit trail
|
|
490
|
+
- Can replay to any point in time
|
|
491
|
+
- Events are facts (immutable)
|
|
492
|
+
|
|
493
|
+
## Practical Patterns
|
|
494
|
+
|
|
495
|
+
### Option Type for Missing Values
|
|
496
|
+
|
|
497
|
+
Don't use null/undefined for domain concepts:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
type Option<T> = { some: true; value: T } | { some: false };
|
|
501
|
+
|
|
502
|
+
function findCustomer(id: CustomerId): Option<Customer> {
|
|
503
|
+
// ...
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Forces handling:
|
|
507
|
+
const result = findCustomer(customerId);
|
|
508
|
+
if (result.some) {
|
|
509
|
+
console.log(result.value.name);
|
|
510
|
+
} else {
|
|
511
|
+
console.log("Customer not found");
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Newtype Pattern for Type Safety
|
|
516
|
+
|
|
517
|
+
Create distinct types from same underlying representation:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
type UserId = string & { __brand: "UserId" };
|
|
521
|
+
type ProductId = string & { __brand: "ProductId" };
|
|
522
|
+
type OrderId = string & { __brand: "OrderId" };
|
|
523
|
+
|
|
524
|
+
// Can't mix up IDs:
|
|
525
|
+
function getUser(id: UserId): User { ... }
|
|
526
|
+
const productId: ProductId = ...;
|
|
527
|
+
// getUser(productId); // Type error!
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Builder Pattern for Complex Construction
|
|
531
|
+
|
|
532
|
+
For complex objects with many fields:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
class OrderBuilder {
|
|
536
|
+
private order: Partial<Order> = {};
|
|
537
|
+
|
|
538
|
+
withCustomer(id: CustomerId): this {
|
|
539
|
+
this.order.customerId = id;
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
addItem(item: OrderLine): this {
|
|
544
|
+
this.order.items = [...(this.order.items || []), item];
|
|
545
|
+
return this;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
build(): Result<Order, ValidationError> {
|
|
549
|
+
if (!this.order.customerId) return Err({ error: "Customer required" });
|
|
550
|
+
if (!this.order.items?.length) return Err({ error: "Items required" });
|
|
551
|
+
// ... validate all required fields present
|
|
552
|
+
return Ok(this.order as Order);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const result = new OrderBuilder()
|
|
557
|
+
.withCustomer(customerId)
|
|
558
|
+
.addItem(item1)
|
|
559
|
+
.addItem(item2)
|
|
560
|
+
.build();
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Active Pattern / Parser Pattern
|
|
564
|
+
|
|
565
|
+
Transform external data into domain types:
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
function parseOrderRequest(json: unknown): Result<UnvalidatedOrder, ParseError> {
|
|
569
|
+
// Parse and validate structure
|
|
570
|
+
if (!isObject(json)) return Err({ error: "Expected object" });
|
|
571
|
+
if (!hasProperty(json, "items")) return Err({ error: "Missing items" });
|
|
572
|
+
// ... more parsing
|
|
573
|
+
return Ok({ items: json.items, customerId: json.customerId });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function handleRequest(req: Request): Response {
|
|
577
|
+
return parseOrderRequest(req.body)
|
|
578
|
+
.flatMap(validateOrder)
|
|
579
|
+
.flatMap(placeOrder)
|
|
580
|
+
.match(
|
|
581
|
+
success => ({ status: 200, body: success }),
|
|
582
|
+
error => ({ status: 400, body: { error } })
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## Domain Modeling Recipes
|
|
588
|
+
|
|
589
|
+
### Recipe: Modeling a Workflow
|
|
590
|
+
|
|
591
|
+
1. **Name the workflow** using ubiquitous language
|
|
592
|
+
2. **Define the input** (unvalidated/raw data)
|
|
593
|
+
3. **Define the output** (result of successful execution)
|
|
594
|
+
4. **Define errors** (what can go wrong)
|
|
595
|
+
5. **Break into steps** (validation, execution, persistence)
|
|
596
|
+
6. **Type each intermediate state**
|
|
597
|
+
7. **Implement as pipeline**
|
|
598
|
+
|
|
599
|
+
### Recipe: Modeling State Transitions
|
|
600
|
+
|
|
601
|
+
1. **List all possible states**
|
|
602
|
+
2. **For each state, determine what data is available**
|
|
603
|
+
3. **Create a discriminated union type**
|
|
604
|
+
4. **Define transition functions** (State → Event → Result<State, Error>)
|
|
605
|
+
5. **Ensure illegal transitions are unrepresentable**
|
|
606
|
+
|
|
607
|
+
### Recipe: Modeling Optional Fields
|
|
608
|
+
|
|
609
|
+
1. **Ask: Is this really optional in all cases?**
|
|
610
|
+
2. **If optional in all cases:** Use Option type
|
|
611
|
+
3. **If required in some states, optional in others:** Use discriminated union with separate states
|
|
612
|
+
4. **Never** use null/undefined for domain optionality
|
|
613
|
+
|
|
614
|
+
### Recipe: Modeling Business Rules
|
|
615
|
+
|
|
616
|
+
1. **Express rule in English**
|
|
617
|
+
2. **Identify what makes the rule satisfied**
|
|
618
|
+
3. **Use types to enforce:** Constrained types, discriminated unions, or validation functions
|
|
619
|
+
4. **Make it impossible to violate:** Better in types than in runtime checks
|
|
620
|
+
|
|
621
|
+
## Key Takeaways
|
|
622
|
+
|
|
623
|
+
1. **Use types to make illegal states unrepresentable**
|
|
624
|
+
2. **Model workflows as pipelines of data transformations**
|
|
625
|
+
3. **Use Result types for operations that can fail**
|
|
626
|
+
4. **Keep domain pure, handle effects at boundaries**
|
|
627
|
+
5. **Design with types first, implementation second**
|
|
628
|
+
6. **Use wrapper types to add semantic meaning**
|
|
629
|
+
7. **Separate commands (writes) from queries (reads)**
|
|
630
|
+
8. **Model events as immutable facts**
|
|
631
|
+
9. **Use Option type instead of null/undefined**
|
|
632
|
+
10. **Let the compiler be your friend - exhaustive pattern matching**
|
|
633
|
+
|
|
634
|
+
When domain modeling, continuously ask:
|
|
635
|
+
- What illegal states can I eliminate?
|
|
636
|
+
- What can go wrong here?
|
|
637
|
+
- Is this type signature telling the truth?
|
|
638
|
+
- Can I make this constraint explicit in the type?
|
|
639
|
+
- What's the simplest type that captures this domain concept?
|