@martinffx/atelier 2.0.0 → 3.0.1
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 +21 -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,585 +0,0 @@
|
|
|
1
|
-
# Option and Result Types for Type-Safe Error Handling
|
|
2
|
-
|
|
3
|
-
Option and Result types make error handling and nullable values explicit in function signatures. They force callers to handle failure cases, eliminating null pointer errors and uncaught exceptions.
|
|
4
|
-
|
|
5
|
-
## Option Type
|
|
6
|
-
|
|
7
|
-
Option represents "a value that may be absent." It's an explicit alternative to `null` or `undefined`.
|
|
8
|
-
|
|
9
|
-
### Definition
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
type None = { _tag: "None" }
|
|
13
|
-
type Some<T> = { _tag: "Some"; value: T }
|
|
14
|
-
type Option<T> = None | Some<T>
|
|
15
|
-
|
|
16
|
-
const None: None = { _tag: "None" }
|
|
17
|
-
const Some = <T>(value: T): Option<T> => ({ _tag: "Some", value })
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### Basic Usage
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
function findUser(id: string): Option<User> {
|
|
24
|
-
const user = database.get(id)
|
|
25
|
-
return user ? Some(user) : None
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const result = findUser("123")
|
|
29
|
-
switch (result._tag) {
|
|
30
|
-
case "Some":
|
|
31
|
-
console.log(`Found user: ${result.value.name}`)
|
|
32
|
-
break
|
|
33
|
-
case "None":
|
|
34
|
-
console.log("User not found")
|
|
35
|
-
break
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### When to Use Option
|
|
40
|
-
|
|
41
|
-
Use Option when:
|
|
42
|
-
- A value may legitimately be absent
|
|
43
|
-
- Absence is an expected, valid outcome (not an error)
|
|
44
|
-
- You want to chain operations that may not find values
|
|
45
|
-
- You want to make nullability explicit in APIs
|
|
46
|
-
|
|
47
|
-
**Example scenarios:**
|
|
48
|
-
- Finding a user by ID (may not exist)
|
|
49
|
-
- Getting a value from a map
|
|
50
|
-
- First/last element of an array
|
|
51
|
-
- Parsing optional configuration fields
|
|
52
|
-
|
|
53
|
-
### Option Utilities
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
// Check variant
|
|
57
|
-
const isNone = <T>(opt: Option<T>): opt is None => opt._tag === "None"
|
|
58
|
-
const isSome = <T>(opt: Option<T>): opt is Some<T> => opt._tag === "Some"
|
|
59
|
-
|
|
60
|
-
// Get value or default
|
|
61
|
-
const getOrElse = <T>(opt: Option<T>, defaultValue: T): T =>
|
|
62
|
-
opt._tag === "Some" ? opt.value : defaultValue
|
|
63
|
-
|
|
64
|
-
// Transform value if present
|
|
65
|
-
const map = <T, U>(opt: Option<T>, fn: (value: T) => U): Option<U> =>
|
|
66
|
-
opt._tag === "Some" ? Some(fn(opt.value)) : None
|
|
67
|
-
|
|
68
|
-
// Chain optional operations
|
|
69
|
-
const flatMap = <T, U>(opt: Option<T>, fn: (value: T) => Option<U>): Option<U> =>
|
|
70
|
-
opt._tag === "Some" ? fn(opt.value) : None
|
|
71
|
-
|
|
72
|
-
// Filter value based on predicate
|
|
73
|
-
const filter = <T>(opt: Option<T>, predicate: (value: T) => boolean): Option<T> =>
|
|
74
|
-
opt._tag === "Some" && predicate(opt.value) ? opt : None
|
|
75
|
-
|
|
76
|
-
// Convert to array (for iteration)
|
|
77
|
-
const toArray = <T>(opt: Option<T>): T[] =>
|
|
78
|
-
opt._tag === "Some" ? [opt.value] : []
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Option Examples
|
|
82
|
-
|
|
83
|
-
#### Configuration Parsing
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
type Config = {
|
|
87
|
-
port: number
|
|
88
|
-
host: string
|
|
89
|
-
logLevel: Option<string> // Optional field
|
|
90
|
-
maxConnections: Option<number>
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function parseConfig(raw: unknown): Config {
|
|
94
|
-
const data = raw as any
|
|
95
|
-
return {
|
|
96
|
-
port: data.port,
|
|
97
|
-
host: data.host,
|
|
98
|
-
logLevel: data.logLevel ? Some(data.logLevel) : None,
|
|
99
|
-
maxConnections: data.maxConnections ? Some(data.maxConnections) : None,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const config = parseConfig({ port: 3000, host: "localhost" })
|
|
104
|
-
|
|
105
|
-
// Use getOrElse for defaults
|
|
106
|
-
const logLevel = getOrElse(config.logLevel, "info")
|
|
107
|
-
const maxConnections = getOrElse(config.maxConnections, 100)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
#### Chaining Operations
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
type User = { id: string; name: string; email: string }
|
|
114
|
-
type Profile = { bio: string; avatar: string }
|
|
115
|
-
|
|
116
|
-
function findUser(id: string): Option<User> {
|
|
117
|
-
// ...
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getProfile(user: User): Option<Profile> {
|
|
121
|
-
// ...
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Chain without nested if statements
|
|
125
|
-
const profile = flatMap(findUser("123"), getProfile)
|
|
126
|
-
|
|
127
|
-
switch (profile._tag) {
|
|
128
|
-
case "Some":
|
|
129
|
-
console.log(profile.value.bio)
|
|
130
|
-
break
|
|
131
|
-
case "None":
|
|
132
|
-
console.log("Profile not found")
|
|
133
|
-
break
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
#### Array Operations
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
function first<T>(arr: T[]): Option<T> {
|
|
141
|
-
return arr.length > 0 ? Some(arr[0]) : None
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function last<T>(arr: T[]): Option<T> {
|
|
145
|
-
return arr.length > 0 ? Some(arr[arr.length - 1]) : None
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const numbers = [1, 2, 3, 4, 5]
|
|
149
|
-
const firstNum = first(numbers) // Some(1)
|
|
150
|
-
const lastNum = last(numbers) // Some(5)
|
|
151
|
-
|
|
152
|
-
const empty: number[] = []
|
|
153
|
-
const noFirst = first(empty) // None
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Result Type
|
|
157
|
-
|
|
158
|
-
Result represents "an operation that may fail with an error." It makes errors explicit in function signatures and forces error handling.
|
|
159
|
-
|
|
160
|
-
### Definition
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
type Ok<T> = { _tag: "Ok"; value: T }
|
|
164
|
-
type Err<E> = { _tag: "Err"; error: E }
|
|
165
|
-
type Result<T, E> = Ok<T> | Err<E>
|
|
166
|
-
|
|
167
|
-
const Ok = <T>(value: T): Result<T, never> => ({ _tag: "Ok", value })
|
|
168
|
-
const Err = <E>(error: E): Result<never, E> => ({ _tag: "Err", error })
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Basic Usage
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
type ParseError = { field: string; message: string }
|
|
175
|
-
|
|
176
|
-
function parsePort(raw: unknown): Result<number, ParseError> {
|
|
177
|
-
if (typeof raw !== "number") {
|
|
178
|
-
return Err({ field: "port", message: "must be number" })
|
|
179
|
-
}
|
|
180
|
-
if (raw < 1 || raw > 65535) {
|
|
181
|
-
return Err({ field: "port", message: "must be 1-65535" })
|
|
182
|
-
}
|
|
183
|
-
return Ok(raw)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const result = parsePort(3000)
|
|
187
|
-
switch (result._tag) {
|
|
188
|
-
case "Ok":
|
|
189
|
-
console.log(`Port: ${result.value}`)
|
|
190
|
-
break
|
|
191
|
-
case "Err":
|
|
192
|
-
console.error(`Error in ${result.error.field}: ${result.error.message}`)
|
|
193
|
-
break
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### When to Use Result
|
|
198
|
-
|
|
199
|
-
Use Result when:
|
|
200
|
-
- An operation may fail with recoverable errors
|
|
201
|
-
- You need to propagate error context (not just "failed")
|
|
202
|
-
- You want errors in function signatures (self-documenting)
|
|
203
|
-
- You want to avoid try/catch for expected failures
|
|
204
|
-
|
|
205
|
-
**Example scenarios:**
|
|
206
|
-
- Parsing/validating user input
|
|
207
|
-
- Network requests
|
|
208
|
-
- File I/O operations
|
|
209
|
-
- Business rule validation
|
|
210
|
-
|
|
211
|
-
### Result Utilities
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
// Check variant
|
|
215
|
-
const isOk = <T, E>(result: Result<T, E>): result is Ok<T> => result._tag === "Ok"
|
|
216
|
-
const isErr = <T, E>(result: Result<T, E>): result is Err<E> => result._tag === "Err"
|
|
217
|
-
|
|
218
|
-
// Get value or throw
|
|
219
|
-
const unwrap = <T, E>(result: Result<T, E>): T => {
|
|
220
|
-
if (result._tag === "Ok") return result.value
|
|
221
|
-
throw new Error(`Unwrap failed: ${JSON.stringify(result.error)}`)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Get value or default
|
|
225
|
-
const getOrElse = <T, E>(result: Result<T, E>, defaultValue: T): T =>
|
|
226
|
-
result._tag === "Ok" ? result.value : defaultValue
|
|
227
|
-
|
|
228
|
-
// Transform success value
|
|
229
|
-
const map = <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> =>
|
|
230
|
-
result._tag === "Ok" ? Ok(fn(result.value)) : result
|
|
231
|
-
|
|
232
|
-
// Transform error
|
|
233
|
-
const mapErr = <T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> =>
|
|
234
|
-
result._tag === "Err" ? Err(fn(result.error)) : result
|
|
235
|
-
|
|
236
|
-
// Chain operations that return Result
|
|
237
|
-
const flatMap = <T, U, E>(
|
|
238
|
-
result: Result<T, E>,
|
|
239
|
-
fn: (value: T) => Result<U, E>
|
|
240
|
-
): Result<U, E> =>
|
|
241
|
-
result._tag === "Ok" ? fn(result.value) : result
|
|
242
|
-
|
|
243
|
-
// Combine multiple Results
|
|
244
|
-
const all = <T, E>(results: Result<T, E>[]): Result<T[], E> => {
|
|
245
|
-
const values: T[] = []
|
|
246
|
-
for (const result of results) {
|
|
247
|
-
if (result._tag === "Err") return result
|
|
248
|
-
values.push(result.value)
|
|
249
|
-
}
|
|
250
|
-
return Ok(values)
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Result Examples
|
|
255
|
-
|
|
256
|
-
#### Configuration Validation
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
type ConfigError = { field: string; message: string }
|
|
260
|
-
|
|
261
|
-
function parsePort(raw: unknown): Result<number, ConfigError> {
|
|
262
|
-
if (typeof raw !== "number") {
|
|
263
|
-
return Err({ field: "port", message: "must be number" })
|
|
264
|
-
}
|
|
265
|
-
if (raw < 1 || raw > 65535) {
|
|
266
|
-
return Err({ field: "port", message: "must be 1-65535" })
|
|
267
|
-
}
|
|
268
|
-
return Ok(raw)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function parseHost(raw: unknown): Result<string, ConfigError> {
|
|
272
|
-
if (typeof raw !== "string") {
|
|
273
|
-
return Err({ field: "host", message: "must be string" })
|
|
274
|
-
}
|
|
275
|
-
if (raw.length === 0) {
|
|
276
|
-
return Err({ field: "host", message: "cannot be empty" })
|
|
277
|
-
}
|
|
278
|
-
return Ok(raw)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
type Config = { port: number; host: string }
|
|
282
|
-
|
|
283
|
-
function parseConfig(raw: unknown): Result<Config, ConfigError> {
|
|
284
|
-
const data = raw as any
|
|
285
|
-
|
|
286
|
-
const portResult = parsePort(data.port)
|
|
287
|
-
if (portResult._tag === "Err") return portResult
|
|
288
|
-
|
|
289
|
-
const hostResult = parseHost(data.host)
|
|
290
|
-
if (hostResult._tag === "Err") return hostResult
|
|
291
|
-
|
|
292
|
-
return Ok({ port: portResult.value, host: hostResult.value })
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Usage
|
|
296
|
-
const configResult = parseConfig({ port: 3000, host: "localhost" })
|
|
297
|
-
switch (configResult._tag) {
|
|
298
|
-
case "Ok":
|
|
299
|
-
startServer(configResult.value)
|
|
300
|
-
break
|
|
301
|
-
case "Err":
|
|
302
|
-
console.error(`Config error in ${configResult.error.field}: ${configResult.error.message}`)
|
|
303
|
-
process.exit(1)
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
#### Chaining Operations
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
type DbError = { kind: "not_found" } | { kind: "connection_error"; message: string }
|
|
311
|
-
|
|
312
|
-
function findUser(id: string): Result<User, DbError> {
|
|
313
|
-
// ...
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function updateUser(user: User, updates: Partial<User>): Result<User, DbError> {
|
|
317
|
-
// ...
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Chain without nested if statements
|
|
321
|
-
const result = flatMap(
|
|
322
|
-
findUser("123"),
|
|
323
|
-
(user) => updateUser(user, { name: "Alice" })
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
switch (result._tag) {
|
|
327
|
-
case "Ok":
|
|
328
|
-
console.log(`Updated user: ${result.value.name}`)
|
|
329
|
-
break
|
|
330
|
-
case "Err":
|
|
331
|
-
if (result.error.kind === "not_found") {
|
|
332
|
-
console.error("User not found")
|
|
333
|
-
} else {
|
|
334
|
-
console.error(`Connection error: ${result.error.message}`)
|
|
335
|
-
}
|
|
336
|
-
break
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
#### HTTP Request Handling
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
type HttpError =
|
|
344
|
-
| { kind: "network"; message: string }
|
|
345
|
-
| { kind: "timeout" }
|
|
346
|
-
| { kind: "server"; statusCode: number }
|
|
347
|
-
| { kind: "parse"; message: string }
|
|
348
|
-
|
|
349
|
-
async function fetchUser(id: string): Promise<Result<User, HttpError>> {
|
|
350
|
-
try {
|
|
351
|
-
const response = await fetch(`/api/users/${id}`)
|
|
352
|
-
|
|
353
|
-
if (!response.ok) {
|
|
354
|
-
return Err({ kind: "server", statusCode: response.status })
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const data = await response.json()
|
|
358
|
-
return Ok(data as User)
|
|
359
|
-
} catch (e) {
|
|
360
|
-
if (e instanceof SyntaxError) {
|
|
361
|
-
return Err({ kind: "parse", message: e.message })
|
|
362
|
-
}
|
|
363
|
-
return Err({ kind: "network", message: String(e) })
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Usage
|
|
368
|
-
const userResult = await fetchUser("123")
|
|
369
|
-
switch (userResult._tag) {
|
|
370
|
-
case "Ok":
|
|
371
|
-
renderUser(userResult.value)
|
|
372
|
-
break
|
|
373
|
-
case "Err":
|
|
374
|
-
switch (userResult.error.kind) {
|
|
375
|
-
case "network":
|
|
376
|
-
showError("Network error, please try again")
|
|
377
|
-
break
|
|
378
|
-
case "timeout":
|
|
379
|
-
showError("Request timed out")
|
|
380
|
-
break
|
|
381
|
-
case "server":
|
|
382
|
-
showError(`Server error: ${userResult.error.statusCode}`)
|
|
383
|
-
break
|
|
384
|
-
case "parse":
|
|
385
|
-
showError("Invalid response from server")
|
|
386
|
-
break
|
|
387
|
-
}
|
|
388
|
-
break
|
|
389
|
-
}
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
#### Accumulating Multiple Errors
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
type ValidationError = { field: string; message: string }
|
|
396
|
-
|
|
397
|
-
function validateEmail(email: string): Result<string, ValidationError> {
|
|
398
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
399
|
-
return Err({ field: "email", message: "invalid email format" })
|
|
400
|
-
}
|
|
401
|
-
return Ok(email)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function validatePassword(password: string): Result<string, ValidationError> {
|
|
405
|
-
if (password.length < 8) {
|
|
406
|
-
return Err({ field: "password", message: "must be at least 8 characters" })
|
|
407
|
-
}
|
|
408
|
-
return Ok(password)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
type SignupForm = { email: string; password: string }
|
|
412
|
-
|
|
413
|
-
function validateSignup(form: SignupForm): Result<SignupForm, ValidationError[]> {
|
|
414
|
-
const errors: ValidationError[] = []
|
|
415
|
-
|
|
416
|
-
const emailResult = validateEmail(form.email)
|
|
417
|
-
if (emailResult._tag === "Err") {
|
|
418
|
-
errors.push(emailResult.error)
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const passwordResult = validatePassword(form.password)
|
|
422
|
-
if (passwordResult._tag === "Err") {
|
|
423
|
-
errors.push(passwordResult.error)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (errors.length > 0) {
|
|
427
|
-
return Err(errors)
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return Ok(form)
|
|
431
|
-
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
## Option vs Result vs Exceptions
|
|
435
|
-
|
|
436
|
-
### Decision Guide
|
|
437
|
-
|
|
438
|
-
| Scenario | Use |
|
|
439
|
-
|----------|-----|
|
|
440
|
-
| Value may be absent (expected) | Option |
|
|
441
|
-
| Operation may fail (recoverable) | Result |
|
|
442
|
-
| Programmer error (bug) | Exception |
|
|
443
|
-
|
|
444
|
-
### Examples
|
|
445
|
-
|
|
446
|
-
```typescript
|
|
447
|
-
// Option: Value may be absent
|
|
448
|
-
function findUser(id: string): Option<User>
|
|
449
|
-
|
|
450
|
-
// Result: Operation may fail
|
|
451
|
-
function parseConfig(raw: string): Result<Config, ParseError>
|
|
452
|
-
|
|
453
|
-
// Exception: Programmer error
|
|
454
|
-
function unreachable(message: string): never {
|
|
455
|
-
throw new Error(`Unreachable: ${message}`)
|
|
456
|
-
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### When to Use Exceptions
|
|
460
|
-
|
|
461
|
-
Use exceptions for:
|
|
462
|
-
- Programmer errors (bugs)
|
|
463
|
-
- Assertion failures
|
|
464
|
-
- Unrecoverable errors
|
|
465
|
-
- Contract violations
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
function assertNever(x: never): never {
|
|
469
|
-
throw new Error(`Unhandled variant: ${JSON.stringify(x)}`)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function divide(a: number, b: number): number {
|
|
473
|
-
if (b === 0) {
|
|
474
|
-
throw new Error("Division by zero") // Programmer error
|
|
475
|
-
}
|
|
476
|
-
return a / b
|
|
477
|
-
}
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
## Converting Between Types
|
|
481
|
-
|
|
482
|
-
### Option to Result
|
|
483
|
-
|
|
484
|
-
```typescript
|
|
485
|
-
function optionToResult<T, E>(opt: Option<T>, error: E): Result<T, E> {
|
|
486
|
-
return opt._tag === "Some" ? Ok(opt.value) : Err(error)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Usage
|
|
490
|
-
const user = findUser("123")
|
|
491
|
-
const result = optionToResult(user, { kind: "not_found" })
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### Result to Option
|
|
495
|
-
|
|
496
|
-
```typescript
|
|
497
|
-
function resultToOption<T, E>(result: Result<T, E>): Option<T> {
|
|
498
|
-
return result._tag === "Ok" ? Some(result.value) : None
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Usage
|
|
502
|
-
const configResult = parseConfig(rawConfig)
|
|
503
|
-
const config = resultToOption(configResult) // Discard error details
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### Null/Undefined to Option
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
function fromNullable<T>(value: T | null | undefined): Option<T> {
|
|
510
|
-
return value != null ? Some(value) : None
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Usage
|
|
514
|
-
const maybePort = fromNullable(process.env.PORT)
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
## Best Practices
|
|
518
|
-
|
|
519
|
-
1. **Make errors explicit in signatures**: `Result<T, E>` tells callers what can fail
|
|
520
|
-
2. **Use specific error types**: Don't use `string` or `Error` as error type
|
|
521
|
-
3. **Combine with discriminated unions**: Error variants should be tagged unions
|
|
522
|
-
4. **Avoid nested Results**: Use `flatMap` to chain operations
|
|
523
|
-
5. **Don't mix Option and null**: Choose one approach per codebase
|
|
524
|
-
6. **Use utilities for common patterns**: Don't rewrite `map`, `flatMap`, etc.
|
|
525
|
-
7. **Document when to use exceptions**: Reserve for programmer errors
|
|
526
|
-
|
|
527
|
-
## Common Patterns
|
|
528
|
-
|
|
529
|
-
### Early Return with Result
|
|
530
|
-
|
|
531
|
-
```typescript
|
|
532
|
-
function processOrder(orderId: string): Result<Order, OrderError> {
|
|
533
|
-
const orderResult = findOrder(orderId)
|
|
534
|
-
if (orderResult._tag === "Err") return orderResult
|
|
535
|
-
|
|
536
|
-
const paymentResult = processPayment(orderResult.value)
|
|
537
|
-
if (paymentResult._tag === "Err") return mapErr(paymentResult, toOrderError)
|
|
538
|
-
|
|
539
|
-
const shipmentResult = createShipment(orderResult.value)
|
|
540
|
-
if (shipmentResult._tag === "Err") return mapErr(shipmentResult, toOrderError)
|
|
541
|
-
|
|
542
|
-
return Ok(orderResult.value)
|
|
543
|
-
}
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
### Combining Multiple Results
|
|
547
|
-
|
|
548
|
-
```typescript
|
|
549
|
-
function validateAll<T>(
|
|
550
|
-
validators: ((value: T) => Result<T, string>)[]
|
|
551
|
-
): (value: T) => Result<T, string[]> {
|
|
552
|
-
return (value) => {
|
|
553
|
-
const errors: string[] = []
|
|
554
|
-
|
|
555
|
-
for (const validator of validators) {
|
|
556
|
-
const result = validator(value)
|
|
557
|
-
if (result._tag === "Err") {
|
|
558
|
-
errors.push(result.error)
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return errors.length > 0 ? Err(errors) : Ok(value)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### Safe Property Access with Option
|
|
568
|
-
|
|
569
|
-
```typescript
|
|
570
|
-
function getProp<T, K extends keyof T>(obj: T, key: K): Option<T[K]> {
|
|
571
|
-
const value = obj[key]
|
|
572
|
-
return value !== undefined ? Some(value) : None
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Usage
|
|
576
|
-
type Config = { port?: number }
|
|
577
|
-
const config: Config = {}
|
|
578
|
-
const port = getProp(config, "port") // Option<number>
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
## Further Reading
|
|
582
|
-
|
|
583
|
-
- [ADTs](./adts.md) - Algebraic Data Types and discriminated unions
|
|
584
|
-
- [Branded Types](./branded-types.md) - Smart constructors and nominal typing
|
|
585
|
-
- [Migration Guide](./migration-guide.md) - Step-by-step adoption playbook
|