@martinffx/atelier 0.1.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 +38 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/agents/architect.md +38 -0
- package/agents/oracle.md +40 -0
- package/agents/recon.md +39 -0
- package/dist/atelier.js +33082 -0
- package/package.json +44 -0
- package/skills/code-conventional-commit/SKILL.md +134 -0
- package/skills/code-conventional-commit/evals/evals.json +23 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
- package/skills/code-debug/SKILL.md +271 -0
- package/skills/code-debug/references/language-tools.md +139 -0
- package/skills/code-debug/references/techniques.md +117 -0
- package/skills/code-docs/SKILL.md +171 -0
- package/skills/code-docs/references/api-docs.md +116 -0
- package/skills/code-docs/references/changelog.md +64 -0
- package/skills/code-docs/references/readme-structure.md +94 -0
- package/skills/code-git-worktrees/SKILL.md +218 -0
- package/skills/code-perf/SKILL.md +163 -0
- package/skills/code-perf/references/common-bottlenecks.md +124 -0
- package/skills/code-perf/references/optimization-patterns.md +167 -0
- package/skills/code-perf/references/profiling-tools.md +126 -0
- package/skills/code-review/SKILL.md +78 -0
- package/skills/code-review/references/output.md +200 -0
- package/skills/code-review/references/reviewers.md +719 -0
- package/skills/code-review/references/rq.md +357 -0
- package/skills/code-review/references/rs.md +30 -0
- package/skills/code-security/SKILL.md +183 -0
- package/skills/code-security/references/owasp-top-10.md +129 -0
- package/skills/code-security/references/security-tools.md +102 -0
- package/skills/code-security/references/vulnerability-patterns.md +129 -0
- package/skills/code-stacked-commit/SKILL.md +155 -0
- package/skills/code-subagents/SKILL.md +184 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
- package/skills/code-subagents/references/implementor-prompt.md +79 -0
- package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
- package/skills/oracle-architect/SKILL.md +246 -0
- package/skills/oracle-architect/references/api-design.md +345 -0
- package/skills/oracle-architect/references/data-modeling.md +318 -0
- package/skills/oracle-architect/references/ddd-patterns.md +247 -0
- package/skills/oracle-challenge/SKILL.md +126 -0
- package/skills/oracle-testing/SKILL.md +237 -0
- package/skills/oracle-testing/references/anti-patterns.md +618 -0
- package/skills/oracle-testing/references/boundaries.md +579 -0
- package/skills/oracle-testing/references/core-testing.md +691 -0
- package/skills/oracle-testing/references/edge-testing.md +794 -0
- package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
- package/skills/oracle-thinkdeep/SKILL.md +164 -0
- package/skills/python-architecture/SKILL.md +148 -0
- package/skills/python-architecture/references/data-modeling.md +558 -0
- package/skills/python-architecture/references/ddd.md +569 -0
- package/skills/python-architecture/references/functional-core.md +337 -0
- package/skills/python-build-tools/SKILL.md +153 -0
- package/skills/python-build-tools/references/basedpyright.md +321 -0
- package/skills/python-build-tools/references/ruff.md +225 -0
- package/skills/python-build-tools/references/uv.md +257 -0
- package/skills/python-fastapi/SKILL.md +184 -0
- package/skills/python-fastapi/references/api-design.md +563 -0
- package/skills/python-fastapi/references/dependencies.md +479 -0
- package/skills/python-fastapi/references/middleware.md +475 -0
- package/skills/python-fastapi/references/validation.md +497 -0
- package/skills/python-modern-python/SKILL.md +201 -0
- package/skills/python-modern-python/references/async.md +76 -0
- package/skills/python-modern-python/references/pattern-matching.md +89 -0
- package/skills/python-modern-python/references/typing.md +96 -0
- package/skills/python-monorepo/SKILL.md +146 -0
- package/skills/python-monorepo/references/docker.md +515 -0
- package/skills/python-monorepo/references/namespace-packages.md +426 -0
- package/skills/python-monorepo/references/workspace-config.md +341 -0
- package/skills/python-sqlalchemy/SKILL.md +119 -0
- package/skills/python-sqlalchemy/references/async.md +51 -0
- package/skills/python-sqlalchemy/references/models.md +67 -0
- package/skills/python-sqlalchemy/references/queries.md +57 -0
- package/skills/python-temporal/SKILL.md +128 -0
- package/skills/python-temporal/references/patterns.md +87 -0
- package/skills/python-temporal/references/testing.md +54 -0
- package/skills/python-testing/SKILL.md +161 -0
- package/skills/python-testing/references/boundaries.md +449 -0
- package/skills/python-testing/references/mocking.md +449 -0
- package/skills/python-testing/references/pytest.md +524 -0
- package/skills/spec-brainstorm/SKILL.md +325 -0
- package/skills/spec-finish/SKILL.md +159 -0
- package/skills/spec-implement/SKILL.md +296 -0
- package/skills/spec-orchestrator/SKILL.md +156 -0
- package/skills/spec-plan/SKILL.md +248 -0
- package/skills/typescript-api-design/SKILL.md +396 -0
- package/skills/typescript-api-design/references/error-responses.md +469 -0
- package/skills/typescript-build-tools/SKILL.md +447 -0
- package/skills/typescript-build-tools/references/biome.md +436 -0
- package/skills/typescript-build-tools/references/bun.md +432 -0
- package/skills/typescript-build-tools/references/turborepo.md +426 -0
- package/skills/typescript-build-tools/references/vitest.md +518 -0
- package/skills/typescript-drizzle-orm/SKILL.md +398 -0
- package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
- package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
- package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
- package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
- package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
- package/skills/typescript-effect-ts/SKILL.md +341 -0
- package/skills/typescript-effect-ts/references/core.md +412 -0
- package/skills/typescript-effect-ts/references/error-handling.md +468 -0
- package/skills/typescript-effect-ts/references/resources.md +476 -0
- package/skills/typescript-effect-ts/references/schema.md +536 -0
- package/skills/typescript-effect-ts/references/services.md +461 -0
- package/skills/typescript-fastify/SKILL.md +405 -0
- package/skills/typescript-fastify/references/plugins.md +329 -0
- package/skills/typescript-fastify/references/typeid.md +315 -0
- package/skills/typescript-functional-patterns/SKILL.md +402 -0
- package/skills/typescript-functional-patterns/references/adts.md +497 -0
- package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
- package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
- package/skills/typescript-functional-patterns/references/option-result.md +585 -0
- package/skills/typescript-testing/SKILL.md +353 -0
- package/skills/typescript-testing/references/mocking.md +675 -0
- package/skills/typescript-testing/references/msw.md +852 -0
- package/skills/typescript-testing/references/snapshot-testing.md +774 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
# Migration Guide: Adopting Functional Patterns
|
|
2
|
+
|
|
3
|
+
This guide provides a practical, incremental approach to introducing functional patterns (ADTs, Option, Result, branded types) into an existing TypeScript codebase.
|
|
4
|
+
|
|
5
|
+
## Migration Philosophy
|
|
6
|
+
|
|
7
|
+
**Start small, prove value, expand gradually.**
|
|
8
|
+
|
|
9
|
+
Don't rewrite everything at once. Instead:
|
|
10
|
+
1. Apply patterns to new features immediately
|
|
11
|
+
2. Refactor existing code opportunistically (when touching it)
|
|
12
|
+
3. Prioritize high-risk areas (financial calculations, state machines)
|
|
13
|
+
4. Build team familiarity through examples and pairing
|
|
14
|
+
|
|
15
|
+
## Stage 1: Setup and Foundation
|
|
16
|
+
|
|
17
|
+
### Enable TypeScript Strict Mode
|
|
18
|
+
|
|
19
|
+
Add these flags to `tsconfig.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"compilerOptions": {
|
|
24
|
+
"strict": true,
|
|
25
|
+
"strictNullChecks": true,
|
|
26
|
+
"noImplicitReturns": true,
|
|
27
|
+
"strictFunctionTypes": true,
|
|
28
|
+
"noUncheckedIndexedAccess": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Why:** These flags make the type system stricter, enabling better safety and forcing you to handle edge cases.
|
|
34
|
+
|
|
35
|
+
**How to migrate:**
|
|
36
|
+
1. Start with `strictNullChecks: true`
|
|
37
|
+
2. Fix all type errors (replace `T` with `T | null | undefined` where needed)
|
|
38
|
+
3. Gradually enable other flags
|
|
39
|
+
4. Use `// @ts-expect-error` for complex migrations (remove later)
|
|
40
|
+
|
|
41
|
+
### Create Utility Files
|
|
42
|
+
|
|
43
|
+
Create a `lib/functional.ts` file with core utilities:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// ============================================
|
|
47
|
+
// Option Type
|
|
48
|
+
// ============================================
|
|
49
|
+
export type None = { _tag: "None" }
|
|
50
|
+
export type Some<T> = { _tag: "Some"; value: T }
|
|
51
|
+
export type Option<T> = None | Some<T>
|
|
52
|
+
|
|
53
|
+
export const None: None = { _tag: "None" }
|
|
54
|
+
export const Some = <T>(value: T): Option<T> => ({ _tag: "Some", value })
|
|
55
|
+
|
|
56
|
+
export const isNone = <T>(opt: Option<T>): opt is None => opt._tag === "None"
|
|
57
|
+
export const isSome = <T>(opt: Option<T>): opt is Some<T> => opt._tag === "Some"
|
|
58
|
+
|
|
59
|
+
export const getOrElse = <T>(opt: Option<T>, defaultValue: T): T =>
|
|
60
|
+
opt._tag === "Some" ? opt.value : defaultValue
|
|
61
|
+
|
|
62
|
+
export const map = <T, U>(opt: Option<T>, fn: (value: T) => U): Option<U> =>
|
|
63
|
+
opt._tag === "Some" ? Some(fn(opt.value)) : None
|
|
64
|
+
|
|
65
|
+
export const flatMap = <T, U>(opt: Option<T>, fn: (value: T) => Option<U>): Option<U> =>
|
|
66
|
+
opt._tag === "Some" ? fn(opt.value) : None
|
|
67
|
+
|
|
68
|
+
// ============================================
|
|
69
|
+
// Result Type
|
|
70
|
+
// ============================================
|
|
71
|
+
export type Ok<T> = { _tag: "Ok"; value: T }
|
|
72
|
+
export type Err<E> = { _tag: "Err"; error: E }
|
|
73
|
+
export type Result<T, E> = Ok<T> | Err<E>
|
|
74
|
+
|
|
75
|
+
export const Ok = <T>(value: T): Result<T, never> => ({ _tag: "Ok", value })
|
|
76
|
+
export const Err = <E>(error: E): Result<never, E> => ({ _tag: "Err", error })
|
|
77
|
+
|
|
78
|
+
export const isOk = <T, E>(result: Result<T, E>): result is Ok<T> => result._tag === "Ok"
|
|
79
|
+
export const isErr = <T, E>(result: Result<T, E>): result is Err<E> => result._tag === "Err"
|
|
80
|
+
|
|
81
|
+
export const mapResult = <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> =>
|
|
82
|
+
result._tag === "Ok" ? Ok(fn(result.value)) : result
|
|
83
|
+
|
|
84
|
+
export const flatMapResult = <T, U, E>(
|
|
85
|
+
result: Result<T, E>,
|
|
86
|
+
fn: (value: T) => Result<U, E>
|
|
87
|
+
): Result<U, E> =>
|
|
88
|
+
result._tag === "Ok" ? fn(result.value) : result
|
|
89
|
+
|
|
90
|
+
// ============================================
|
|
91
|
+
// Exhaustiveness Checking
|
|
92
|
+
// ============================================
|
|
93
|
+
export const assertNever = (x: never): never => {
|
|
94
|
+
throw new Error(`Unhandled variant: ${JSON.stringify(x)}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// Branded Types
|
|
99
|
+
// ============================================
|
|
100
|
+
export type Brand<K, T> = K & { __brand: T }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Import from this file across your codebase:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { Option, Some, None, Result, Ok, Err } from "@/lib/functional"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Stage 2: Identify Code Smells
|
|
110
|
+
|
|
111
|
+
Look for these patterns that benefit from functional refactoring:
|
|
112
|
+
|
|
113
|
+
### 1. Boolean Soup
|
|
114
|
+
|
|
115
|
+
**Before:**
|
|
116
|
+
```typescript
|
|
117
|
+
type Order = {
|
|
118
|
+
id: string
|
|
119
|
+
isPaid: boolean
|
|
120
|
+
isShipped: boolean
|
|
121
|
+
isCancelled: boolean
|
|
122
|
+
}
|
|
123
|
+
// What does { isPaid: true, isCancelled: true } mean?
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**After:**
|
|
127
|
+
```typescript
|
|
128
|
+
type OrderStatus =
|
|
129
|
+
| { kind: "pending" }
|
|
130
|
+
| { kind: "paid"; paidAt: Date }
|
|
131
|
+
| { kind: "shipped"; trackingNumber: string }
|
|
132
|
+
| { kind: "delivered"; deliveredAt: Date }
|
|
133
|
+
| { kind: "cancelled"; reason: string }
|
|
134
|
+
|
|
135
|
+
type Order = {
|
|
136
|
+
id: string
|
|
137
|
+
status: OrderStatus
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 2. Null/Undefined Checks
|
|
142
|
+
|
|
143
|
+
**Before:**
|
|
144
|
+
```typescript
|
|
145
|
+
function findUser(id: string): User | null {
|
|
146
|
+
// ...
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const user = findUser("123")
|
|
150
|
+
if (user !== null) {
|
|
151
|
+
console.log(user.name)
|
|
152
|
+
} else {
|
|
153
|
+
console.log("Not found")
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**After:**
|
|
158
|
+
```typescript
|
|
159
|
+
function findUser(id: string): Option<User> {
|
|
160
|
+
const user = database.get(id)
|
|
161
|
+
return user ? Some(user) : None
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const user = findUser("123")
|
|
165
|
+
switch (user._tag) {
|
|
166
|
+
case "Some":
|
|
167
|
+
console.log(user.value.name)
|
|
168
|
+
break
|
|
169
|
+
case "None":
|
|
170
|
+
console.log("Not found")
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 3. Error Handling with Exceptions
|
|
176
|
+
|
|
177
|
+
**Before:**
|
|
178
|
+
```typescript
|
|
179
|
+
function parseConfig(raw: string): Config {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(raw)
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Caller doesn't know this throws
|
|
184
|
+
throw new Error("Invalid config")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**After:**
|
|
190
|
+
```typescript
|
|
191
|
+
type ParseError = { message: string; cause?: unknown }
|
|
192
|
+
|
|
193
|
+
function parseConfig(raw: string): Result<Config, ParseError> {
|
|
194
|
+
try {
|
|
195
|
+
const data = JSON.parse(raw)
|
|
196
|
+
return Ok(data)
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return Err({ message: "Invalid config", cause: e })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 4. Primitive Obsession
|
|
204
|
+
|
|
205
|
+
**Before:**
|
|
206
|
+
```typescript
|
|
207
|
+
function calculateTotal(priceInCents: number, taxInDollars: number): number {
|
|
208
|
+
// Mix of units - error-prone!
|
|
209
|
+
return priceInCents + taxInDollars * 100
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**After:**
|
|
214
|
+
```typescript
|
|
215
|
+
type Cents = Brand<number, "Cents">
|
|
216
|
+
const Cents = (n: number): Cents => {
|
|
217
|
+
if (!Number.isInteger(n) || n < 0) throw new Error("Invalid cents")
|
|
218
|
+
return n as Cents
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function calculateTotal(price: Cents, tax: Cents): Cents {
|
|
222
|
+
return Cents(price + tax) // Type-safe
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Stage 3: Incremental Adoption Strategy
|
|
227
|
+
|
|
228
|
+
### Week 1-2: New Features Only
|
|
229
|
+
|
|
230
|
+
Apply patterns to all new features:
|
|
231
|
+
- Use discriminated unions for state machines
|
|
232
|
+
- Use Option for nullable values
|
|
233
|
+
- Use Result for operations that may fail
|
|
234
|
+
- Use branded types for domain IDs and units
|
|
235
|
+
|
|
236
|
+
**Goal:** Build team familiarity without touching existing code.
|
|
237
|
+
|
|
238
|
+
### Week 3-4: Bug Fixes and Refactoring
|
|
239
|
+
|
|
240
|
+
When fixing bugs, refactor to functional patterns:
|
|
241
|
+
- Boolean soup → Discriminated unions
|
|
242
|
+
- Null checks → Option
|
|
243
|
+
- Try/catch → Result
|
|
244
|
+
- Primitive types → Branded types
|
|
245
|
+
|
|
246
|
+
**Goal:** Improve code quality while fixing issues.
|
|
247
|
+
|
|
248
|
+
### Week 5-8: High-Risk Areas
|
|
249
|
+
|
|
250
|
+
Prioritize areas where bugs have high impact:
|
|
251
|
+
- **Financial calculations**: Use branded types for Cents, Dollars, BasisPoints
|
|
252
|
+
- **State machines**: Use discriminated unions for transaction states
|
|
253
|
+
- **Authentication**: Use branded types for tokens, session IDs
|
|
254
|
+
- **Configuration**: Use Result for parsing and validation
|
|
255
|
+
|
|
256
|
+
**Goal:** Reduce risk in critical paths.
|
|
257
|
+
|
|
258
|
+
### Month 3+: Gradual Expansion
|
|
259
|
+
|
|
260
|
+
Continue applying patterns when touching code. Measure impact:
|
|
261
|
+
- Fewer null pointer errors
|
|
262
|
+
- Fewer unit confusion bugs
|
|
263
|
+
- Fewer missing case errors (exhaustiveness)
|
|
264
|
+
- Easier refactoring (type safety)
|
|
265
|
+
|
|
266
|
+
## Stage 4: Pattern-Specific Migration
|
|
267
|
+
|
|
268
|
+
### Migrating to Discriminated Unions
|
|
269
|
+
|
|
270
|
+
**Step 1: Identify boolean soup**
|
|
271
|
+
```typescript
|
|
272
|
+
// Before
|
|
273
|
+
type User = {
|
|
274
|
+
isActive: boolean
|
|
275
|
+
isBanned: boolean
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Step 2: Define discriminated union**
|
|
280
|
+
```typescript
|
|
281
|
+
// After
|
|
282
|
+
type UserStatus =
|
|
283
|
+
| { kind: "active" }
|
|
284
|
+
| { kind: "banned"; reason: string; bannedAt: Date }
|
|
285
|
+
| { kind: "inactive"; inactiveAt: Date }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Step 3: Update type definition**
|
|
289
|
+
```typescript
|
|
290
|
+
type User = {
|
|
291
|
+
id: UserId
|
|
292
|
+
email: Email
|
|
293
|
+
status: UserStatus // Replace booleans with status
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Step 4: Update creation code**
|
|
298
|
+
```typescript
|
|
299
|
+
// Before
|
|
300
|
+
const user = { id: "123", isActive: true, isBanned: false }
|
|
301
|
+
|
|
302
|
+
// After
|
|
303
|
+
const user = { id: UserId("123"), email: Email("user@example.com"), status: { kind: "active" } }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Step 5: Update consumption code**
|
|
307
|
+
```typescript
|
|
308
|
+
// Before
|
|
309
|
+
if (user.isActive && !user.isBanned) {
|
|
310
|
+
allowLogin(user)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// After
|
|
314
|
+
switch (user.status.kind) {
|
|
315
|
+
case "active":
|
|
316
|
+
allowLogin(user)
|
|
317
|
+
break
|
|
318
|
+
case "banned":
|
|
319
|
+
showBannedMessage(user.status.reason)
|
|
320
|
+
break
|
|
321
|
+
case "inactive":
|
|
322
|
+
showInactiveMessage()
|
|
323
|
+
break
|
|
324
|
+
default:
|
|
325
|
+
assertNever(user.status)
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Migrating to Option
|
|
330
|
+
|
|
331
|
+
**Step 1: Identify nullable types**
|
|
332
|
+
```typescript
|
|
333
|
+
function findUser(id: string): User | null
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Step 2: Change return type to Option**
|
|
337
|
+
```typescript
|
|
338
|
+
function findUser(id: string): Option<User>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Step 3: Update implementation**
|
|
342
|
+
```typescript
|
|
343
|
+
// Before
|
|
344
|
+
function findUser(id: string): User | null {
|
|
345
|
+
const user = database.get(id)
|
|
346
|
+
return user ?? null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// After
|
|
350
|
+
function findUser(id: string): Option<User> {
|
|
351
|
+
const user = database.get(id)
|
|
352
|
+
return user ? Some(user) : None
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Step 4: Update call sites**
|
|
357
|
+
```typescript
|
|
358
|
+
// Before
|
|
359
|
+
const user = findUser("123")
|
|
360
|
+
if (user !== null) {
|
|
361
|
+
console.log(user.name)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// After
|
|
365
|
+
const user = findUser("123")
|
|
366
|
+
switch (user._tag) {
|
|
367
|
+
case "Some":
|
|
368
|
+
console.log(user.value.name)
|
|
369
|
+
break
|
|
370
|
+
case "None":
|
|
371
|
+
console.log("Not found")
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Migrating to Result
|
|
377
|
+
|
|
378
|
+
**Step 1: Identify throwing functions**
|
|
379
|
+
```typescript
|
|
380
|
+
function parseConfig(raw: string): Config {
|
|
381
|
+
// Throws on error
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Step 2: Define error type**
|
|
386
|
+
```typescript
|
|
387
|
+
type ParseError = { message: string; field?: string }
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Step 3: Change return type to Result**
|
|
391
|
+
```typescript
|
|
392
|
+
function parseConfig(raw: string): Result<Config, ParseError>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Step 4: Update implementation**
|
|
396
|
+
```typescript
|
|
397
|
+
// Before
|
|
398
|
+
function parseConfig(raw: string): Config {
|
|
399
|
+
try {
|
|
400
|
+
return JSON.parse(raw)
|
|
401
|
+
} catch (e) {
|
|
402
|
+
throw new Error("Invalid config")
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// After
|
|
407
|
+
function parseConfig(raw: string): Result<Config, ParseError> {
|
|
408
|
+
try {
|
|
409
|
+
const data = JSON.parse(raw)
|
|
410
|
+
return Ok(data)
|
|
411
|
+
} catch (e) {
|
|
412
|
+
return Err({ message: "Invalid JSON" })
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Step 5: Update call sites**
|
|
418
|
+
```typescript
|
|
419
|
+
// Before
|
|
420
|
+
try {
|
|
421
|
+
const config = parseConfig(rawConfig)
|
|
422
|
+
startServer(config)
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.error("Config error:", e)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// After
|
|
428
|
+
const configResult = parseConfig(rawConfig)
|
|
429
|
+
switch (configResult._tag) {
|
|
430
|
+
case "Ok":
|
|
431
|
+
startServer(configResult.value)
|
|
432
|
+
break
|
|
433
|
+
case "Err":
|
|
434
|
+
console.error("Config error:", configResult.error.message)
|
|
435
|
+
break
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Migrating to Branded Types
|
|
440
|
+
|
|
441
|
+
**Step 1: Identify primitive obsession**
|
|
442
|
+
```typescript
|
|
443
|
+
function getUser(id: string): User
|
|
444
|
+
function getOrder(id: string): Order
|
|
445
|
+
|
|
446
|
+
// Problem: Can mix up IDs
|
|
447
|
+
const orderId = "ord_123"
|
|
448
|
+
getUser(orderId) // Compiles but wrong!
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Step 2: Define branded types**
|
|
452
|
+
```typescript
|
|
453
|
+
type UserId = Brand<string, "UserId">
|
|
454
|
+
type OrderId = Brand<string, "OrderId">
|
|
455
|
+
|
|
456
|
+
const UserId = (s: string): UserId => {
|
|
457
|
+
if (!/^usr_/.test(s)) throw new Error("Invalid UserId")
|
|
458
|
+
return s as UserId
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const OrderId = (s: string): OrderId => {
|
|
462
|
+
if (!/^ord_/.test(s)) throw new Error("Invalid OrderId")
|
|
463
|
+
return s as OrderId
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Step 3: Update function signatures**
|
|
468
|
+
```typescript
|
|
469
|
+
function getUser(id: UserId): User
|
|
470
|
+
function getOrder(id: OrderId): Order
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Step 4: Update call sites**
|
|
474
|
+
```typescript
|
|
475
|
+
// Before
|
|
476
|
+
const userId = "usr_123"
|
|
477
|
+
getUser(userId)
|
|
478
|
+
|
|
479
|
+
// After
|
|
480
|
+
const userId = UserId("usr_123")
|
|
481
|
+
getUser(userId)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Stage 5: CI/CD Enforcement
|
|
485
|
+
|
|
486
|
+
### Linting Rules
|
|
487
|
+
|
|
488
|
+
Create ESLint rules to enforce patterns:
|
|
489
|
+
|
|
490
|
+
```javascript
|
|
491
|
+
// .eslintrc.js
|
|
492
|
+
module.exports = {
|
|
493
|
+
rules: {
|
|
494
|
+
// Prefer switch over if-else for discriminated unions
|
|
495
|
+
"no-else-return": "error",
|
|
496
|
+
|
|
497
|
+
// Require default case in switch
|
|
498
|
+
"default-case": "error",
|
|
499
|
+
},
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Type Coverage
|
|
504
|
+
|
|
505
|
+
Measure type safety with `type-coverage`:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
npm install -D type-coverage
|
|
509
|
+
npx type-coverage --at-least 95
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Pre-commit Hooks
|
|
513
|
+
|
|
514
|
+
Use `husky` to run type checks before commit:
|
|
515
|
+
|
|
516
|
+
```json
|
|
517
|
+
{
|
|
518
|
+
"husky": {
|
|
519
|
+
"hooks": {
|
|
520
|
+
"pre-commit": "tsc --noEmit"
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Stage 6: Team Adoption
|
|
527
|
+
|
|
528
|
+
### Documentation
|
|
529
|
+
|
|
530
|
+
Create internal docs with examples:
|
|
531
|
+
- Why we use these patterns (reliability, safety)
|
|
532
|
+
- When to use each pattern (decision guide)
|
|
533
|
+
- How to migrate existing code (this guide)
|
|
534
|
+
- Common mistakes and solutions
|
|
535
|
+
|
|
536
|
+
### Code Reviews
|
|
537
|
+
|
|
538
|
+
Review checklist:
|
|
539
|
+
- [ ] New state modeled with discriminated unions (not booleans)
|
|
540
|
+
- [ ] Nullable values use Option (not `| null`)
|
|
541
|
+
- [ ] Fallible operations use Result (not throwing)
|
|
542
|
+
- [ ] Domain IDs use branded types (not raw strings)
|
|
543
|
+
- [ ] All switch statements have `assertNever` in default case
|
|
544
|
+
- [ ] Smart constructors validate invariants
|
|
545
|
+
|
|
546
|
+
### Pairing Sessions
|
|
547
|
+
|
|
548
|
+
Pair with team members on:
|
|
549
|
+
- First discriminated union implementation
|
|
550
|
+
- First Option/Result migration
|
|
551
|
+
- First branded type creation
|
|
552
|
+
- Code review of functional patterns
|
|
553
|
+
|
|
554
|
+
### Internal Talks
|
|
555
|
+
|
|
556
|
+
Host brown-bag sessions:
|
|
557
|
+
- "Why functional patterns matter" (30 min)
|
|
558
|
+
- "Migrating to discriminated unions" (45 min)
|
|
559
|
+
- "Option vs Result vs Exceptions" (30 min)
|
|
560
|
+
- "Branded types for domain modeling" (45 min)
|
|
561
|
+
|
|
562
|
+
## Common Migration Challenges
|
|
563
|
+
|
|
564
|
+
### Challenge 1: "Too much boilerplate"
|
|
565
|
+
|
|
566
|
+
**Problem:** Pattern matching feels verbose compared to `if (x != null)`.
|
|
567
|
+
|
|
568
|
+
**Solution:** Accept some verbosity for safety. The compiler proves correctness. Consider:
|
|
569
|
+
- Null checks miss cases (forgot to check before accessing)
|
|
570
|
+
- Switch with `assertNever` proves exhaustiveness
|
|
571
|
+
- Cost: 2 extra lines. Benefit: Compiler-proven correctness.
|
|
572
|
+
|
|
573
|
+
### Challenge 2: "Existing code uses null everywhere"
|
|
574
|
+
|
|
575
|
+
**Problem:** Can't migrate everything at once.
|
|
576
|
+
|
|
577
|
+
**Solution:** Create adapter functions:
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
// Adapter for legacy code
|
|
581
|
+
function optionToNullable<T>(opt: Option<T>): T | null {
|
|
582
|
+
return opt._tag === "Some" ? opt.value : null
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function nullableToOption<T>(value: T | null): Option<T> {
|
|
586
|
+
return value != null ? Some(value) : None
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Use at boundaries
|
|
590
|
+
const user = findUser("123") // Returns Option<User>
|
|
591
|
+
legacyFunction(optionToNullable(user)) // Legacy expects User | null
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Challenge 3: "Team not convinced"
|
|
595
|
+
|
|
596
|
+
**Problem:** Developers resistant to change.
|
|
597
|
+
|
|
598
|
+
**Solution:**
|
|
599
|
+
1. Start with new features (no risk to existing code)
|
|
600
|
+
2. Show concrete bug prevented by pattern (e.g., missing switch case)
|
|
601
|
+
3. Measure impact: fewer null errors, fewer unit mix-ups
|
|
602
|
+
4. Demonstrate refactoring safety (add variant, compiler shows all needed changes)
|
|
603
|
+
5. Share success stories from team
|
|
604
|
+
|
|
605
|
+
### Challenge 4: "Performance concerns"
|
|
606
|
+
|
|
607
|
+
**Problem:** Worried about extra object allocations.
|
|
608
|
+
|
|
609
|
+
**Solution:**
|
|
610
|
+
- Branded types have zero runtime cost (compile-time only)
|
|
611
|
+
- Option/Result overhead is negligible (one extra property)
|
|
612
|
+
- TypeScript optimizes discriminated unions well
|
|
613
|
+
- Profile before optimizing
|
|
614
|
+
- Correctness > micro-optimizations in most code
|
|
615
|
+
|
|
616
|
+
### Challenge 5: "JSON serialization breaks"
|
|
617
|
+
|
|
618
|
+
**Problem:** Branded types don't serialize to JSON correctly.
|
|
619
|
+
|
|
620
|
+
**Solution:** Create serialization helpers:
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
type UserId = Brand<string, "UserId">
|
|
624
|
+
|
|
625
|
+
// Serialize: strip brand (it's just a string)
|
|
626
|
+
function userIdToJSON(id: UserId): string {
|
|
627
|
+
return id
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Deserialize: validate and brand
|
|
631
|
+
function userIdFromJSON(json: string): Result<UserId, string> {
|
|
632
|
+
if (!/^usr_/.test(json)) {
|
|
633
|
+
return Err("Invalid UserId format")
|
|
634
|
+
}
|
|
635
|
+
return Ok(json as UserId)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// In practice
|
|
639
|
+
const json = JSON.stringify({ userId: userIdToJSON(userId) })
|
|
640
|
+
const parsed = JSON.parse(json)
|
|
641
|
+
const userIdResult = userIdFromJSON(parsed.userId)
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
## Measuring Success
|
|
645
|
+
|
|
646
|
+
Track these metrics before and after adoption:
|
|
647
|
+
|
|
648
|
+
### Bugs Prevented
|
|
649
|
+
- Null pointer errors
|
|
650
|
+
- Unit confusion (cents vs dollars)
|
|
651
|
+
- Missing switch cases
|
|
652
|
+
- Invalid state combinations (boolean soup)
|
|
653
|
+
|
|
654
|
+
### Code Quality
|
|
655
|
+
- Type coverage percentage
|
|
656
|
+
- Number of `any` types
|
|
657
|
+
- Number of `// @ts-ignore` comments
|
|
658
|
+
|
|
659
|
+
### Developer Experience
|
|
660
|
+
- Time to refactor features (should decrease)
|
|
661
|
+
- Confidence in refactoring (should increase)
|
|
662
|
+
- Time spent debugging type errors (should decrease)
|
|
663
|
+
- Time to onboard new developers (may increase initially, then decrease)
|
|
664
|
+
|
|
665
|
+
## Timeline Example
|
|
666
|
+
|
|
667
|
+
**Month 1:**
|
|
668
|
+
- Enable strict mode
|
|
669
|
+
- Create utility files
|
|
670
|
+
- Apply to 2-3 new features
|
|
671
|
+
- Team training (brown-bag)
|
|
672
|
+
|
|
673
|
+
**Month 2:**
|
|
674
|
+
- Apply to all new features
|
|
675
|
+
- Refactor 5-10 bug fixes
|
|
676
|
+
- Pair with team members
|
|
677
|
+
- Measure initial impact
|
|
678
|
+
|
|
679
|
+
**Month 3:**
|
|
680
|
+
- Migrate high-risk areas (financial, auth)
|
|
681
|
+
- Expand to 50% of active development
|
|
682
|
+
- Add CI checks
|
|
683
|
+
- Review and iterate
|
|
684
|
+
|
|
685
|
+
**Month 4+:**
|
|
686
|
+
- Standard practice for all code
|
|
687
|
+
- Continue opportunistic refactoring
|
|
688
|
+
- Measure long-term impact
|
|
689
|
+
- Share learnings
|
|
690
|
+
|
|
691
|
+
## Further Reading
|
|
692
|
+
|
|
693
|
+
- [ADTs](./adts.md) - Algebraic Data Types and discriminated unions
|
|
694
|
+
- [Option & Result](./option-result.md) - Type-safe error handling
|
|
695
|
+
- [Branded Types](./branded-types.md) - Smart constructors and nominal typing
|