functype 0.9.0 → 0.9.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/dist/{Either-C-PDWX2U.d.ts → Either-BHep7I0d.d.ts} +7 -2
- package/dist/{Serializable-D9GKEo30.d.ts → Serializable-BbKuhDDL.d.ts} +14 -3
- package/dist/branded/index.d.ts +4 -6
- package/dist/branded/index.mjs +1 -1
- package/dist/chunk-GHBOC52G.mjs +43 -0
- package/dist/chunk-GHBOC52G.mjs.map +1 -0
- package/dist/chunk-R2TQJN3P.mjs +2 -0
- package/dist/chunk-R2TQJN3P.mjs.map +1 -0
- package/dist/either/index.d.ts +2 -2
- package/dist/either/index.mjs +1 -1
- package/dist/fpromise/index.d.ts +2 -2
- package/dist/fpromise/index.mjs +1 -1
- package/dist/index.d.ts +28 -20
- package/dist/index.mjs +1 -1
- package/dist/list/index.d.ts +2 -2
- package/dist/list/index.mjs +1 -1
- package/dist/map/index.d.ts +2 -2
- package/dist/map/index.mjs +1 -1
- package/dist/option/index.d.ts +2 -2
- package/dist/option/index.mjs +1 -1
- package/dist/set/index.d.ts +2 -2
- package/dist/set/index.mjs +1 -1
- package/dist/try/index.d.ts +2 -2
- package/dist/try/index.mjs +1 -1
- package/dist/tuple/index.d.ts +1 -1
- package/package.json +3 -3
- package/readme/BUNDLE_OPTIMIZATION.md +74 -0
- package/readme/FPromise-Assessment.md +43 -0
- package/readme/HKT.md +110 -0
- package/readme/ROADMAP.md +113 -0
- package/readme/TASK-TODO.md +33 -0
- package/readme/TUPLE-EXAMPLES.md +76 -0
- package/readme/TaskMigration.md +129 -0
- package/readme/ai-guide.md +406 -0
- package/readme/examples.md +2093 -0
- package/readme/quick-reference.md +514 -0
- package/readme/task-error-handling.md +283 -0
- package/readme/tasks.md +203 -0
- package/readme/type-index.md +238 -0
- package/dist/chunk-4EYCKDDF.mjs +0 -43
- package/dist/chunk-4EYCKDDF.mjs.map +0 -1
- package/dist/chunk-V6LFV5LW.mjs +0 -2
- package/dist/chunk-V6LFV5LW.mjs.map +0 -1
|
@@ -0,0 +1,2093 @@
|
|
|
1
|
+
# Functype Examples
|
|
2
|
+
|
|
3
|
+
This document provides comprehensive examples of using the Functype library. These examples are designed to help you understand the core concepts and practical applications of functional programming with TypeScript.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Option](#option)
|
|
8
|
+
- [Either](#either)
|
|
9
|
+
- [Try](#try)
|
|
10
|
+
- [List](#list)
|
|
11
|
+
- [Map](#map)
|
|
12
|
+
- [Set](#set)
|
|
13
|
+
- [FPromise](#fpromise)
|
|
14
|
+
- [Task](#task)
|
|
15
|
+
- [Branded Types](#branded-types)
|
|
16
|
+
- [Tuple](#tuple)
|
|
17
|
+
- [Foldable](#foldable)
|
|
18
|
+
- [Matchable](#matchable)
|
|
19
|
+
- [Common Patterns](#common-patterns)
|
|
20
|
+
- [Error Handling](#error-handling)
|
|
21
|
+
- [Real-World Examples](#real-world-examples)
|
|
22
|
+
|
|
23
|
+
## Option
|
|
24
|
+
|
|
25
|
+
The `Option` type represents a value that may or may not exist, similar to Maybe in other functional languages.
|
|
26
|
+
|
|
27
|
+
### Basic Usage
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Option, Some, None } from "functype"
|
|
31
|
+
|
|
32
|
+
// Creating Options
|
|
33
|
+
const withValue = Option(42) // Some(42)
|
|
34
|
+
const withoutValue = Option(null) // None
|
|
35
|
+
const withoutValue2 = Option(undefined) // None
|
|
36
|
+
const explicit1 = Some(42) // Some(42)
|
|
37
|
+
const explicit2 = None() // None
|
|
38
|
+
|
|
39
|
+
// Check if value exists
|
|
40
|
+
console.log(withValue.isDefined()) // true
|
|
41
|
+
console.log(withoutValue.isDefined()) // false
|
|
42
|
+
console.log(withValue.isEmpty()) // false
|
|
43
|
+
console.log(withoutValue.isEmpty()) // true
|
|
44
|
+
|
|
45
|
+
// Safe access to values
|
|
46
|
+
console.log(withValue.get()) // 42
|
|
47
|
+
// console.log(withoutValue.get()) // Would throw error - use getOrElse instead
|
|
48
|
+
|
|
49
|
+
// Default values
|
|
50
|
+
console.log(withValue.getOrElse(0)) // 42
|
|
51
|
+
console.log(withoutValue.getOrElse(0)) // 0
|
|
52
|
+
|
|
53
|
+
// Optional chaining alternative
|
|
54
|
+
const user = Option({ name: "John", address: { city: "New York" } })
|
|
55
|
+
const noUser = Option(null)
|
|
56
|
+
|
|
57
|
+
// With Option
|
|
58
|
+
const city1 = user
|
|
59
|
+
.flatMap((u) => Option(u.address))
|
|
60
|
+
.flatMap((a) => Option(a.city))
|
|
61
|
+
.getOrElse("Unknown")
|
|
62
|
+
const city2 = noUser
|
|
63
|
+
.flatMap((u) => Option(u.address))
|
|
64
|
+
.flatMap((a) => Option(a.city))
|
|
65
|
+
.getOrElse("Unknown")
|
|
66
|
+
|
|
67
|
+
console.log(city1) // "New York"
|
|
68
|
+
console.log(city2) // "Unknown"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Transformations
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { Option } from "functype"
|
|
75
|
+
|
|
76
|
+
const opt1 = Option(5)
|
|
77
|
+
const opt2 = Option(null)
|
|
78
|
+
|
|
79
|
+
// Map: transform the value if present
|
|
80
|
+
const doubled = opt1.map((x) => x * 2) // Some(10)
|
|
81
|
+
const doubledEmpty = opt2.map((x) => x * 2) // None
|
|
82
|
+
|
|
83
|
+
// FlatMap: chain operations that return Options
|
|
84
|
+
const stringLength = (s: string): Option<number> => (s ? Option(s.length) : None())
|
|
85
|
+
|
|
86
|
+
const name = Option("Alice")
|
|
87
|
+
const nameLength = name.flatMap(stringLength) // Some(5)
|
|
88
|
+
|
|
89
|
+
// Filter: keep value only if predicate is true
|
|
90
|
+
const greaterThan3 = opt1.filter((x) => x > 3) // Some(5)
|
|
91
|
+
const lessThan3 = opt1.filter((x) => x < 3) // None
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Pattern Matching
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { Option } from "functype"
|
|
98
|
+
|
|
99
|
+
const opt = Option(42)
|
|
100
|
+
const empty = Option(null)
|
|
101
|
+
|
|
102
|
+
// Using fold for pattern matching
|
|
103
|
+
const result1 = opt.fold(
|
|
104
|
+
() => "No value",
|
|
105
|
+
(value) => `Value: ${value}`,
|
|
106
|
+
)
|
|
107
|
+
console.log(result1) // "Value: 42"
|
|
108
|
+
|
|
109
|
+
const result2 = empty.fold(
|
|
110
|
+
() => "No value",
|
|
111
|
+
(value) => `Value: ${value}`,
|
|
112
|
+
)
|
|
113
|
+
console.log(result2) // "No value"
|
|
114
|
+
|
|
115
|
+
// Using match for pattern matching
|
|
116
|
+
const display1 = opt.match({
|
|
117
|
+
Some: (value) => `Found: ${value}`,
|
|
118
|
+
None: () => "Not found",
|
|
119
|
+
})
|
|
120
|
+
console.log(display1) // "Found: 42"
|
|
121
|
+
|
|
122
|
+
const display2 = empty.match({
|
|
123
|
+
Some: (value) => `Found: ${value}`,
|
|
124
|
+
None: () => "Not found",
|
|
125
|
+
})
|
|
126
|
+
console.log(display2) // "Not found"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Factory Methods
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Option } from "functype"
|
|
133
|
+
|
|
134
|
+
// Creating from nullable values
|
|
135
|
+
const fromNullable = Option.fromNullable(null) // None
|
|
136
|
+
const fromValue = Option.fromNullable(42) // Some(42)
|
|
137
|
+
|
|
138
|
+
// Creating from predicates
|
|
139
|
+
const fromPredicate = Option.fromPredicate(42, (n) => n > 0) // Some(42)
|
|
140
|
+
|
|
141
|
+
const fromFalsePredicate = Option.fromPredicate(-5, (n) => n > 0) // None
|
|
142
|
+
|
|
143
|
+
// Creating from try-catch blocks
|
|
144
|
+
const fromTry = Option.fromTry(() => JSON.parse('{"name":"John"}')) // Some({name: "John"})
|
|
145
|
+
const fromBadTry = Option.fromTry(() => JSON.parse("invalid json")) // None
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Either
|
|
149
|
+
|
|
150
|
+
The `Either` type represents a value of one of two possible types, typically used for error handling.
|
|
151
|
+
|
|
152
|
+
### Basic Usage
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { Either, Left, Right } from "functype"
|
|
156
|
+
|
|
157
|
+
// Creating Either values
|
|
158
|
+
const success = Right<string, number>(42) // Right<number>(42)
|
|
159
|
+
const failure = Left<string, number>("error") // Left<string>("error")
|
|
160
|
+
|
|
161
|
+
// Check variants
|
|
162
|
+
console.log(success.isRight()) // true
|
|
163
|
+
console.log(success.isLeft()) // false
|
|
164
|
+
console.log(failure.isRight()) // false
|
|
165
|
+
console.log(failure.isLeft()) // true
|
|
166
|
+
|
|
167
|
+
// Safe access to values
|
|
168
|
+
console.log(success.get()) // 42
|
|
169
|
+
// console.log(failure.get()) // Would throw error - use getOrElse instead
|
|
170
|
+
|
|
171
|
+
// Default values
|
|
172
|
+
console.log(success.getOrElse(0)) // 42
|
|
173
|
+
console.log(failure.getOrElse(0)) // 0
|
|
174
|
+
|
|
175
|
+
// Get the left value
|
|
176
|
+
console.log(failure.getLeft()) // "error"
|
|
177
|
+
// console.log(success.getLeft()) // Would throw error
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Transformations
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { Either, Left, Right } from "functype"
|
|
184
|
+
|
|
185
|
+
const success = Right<string, number>(5)
|
|
186
|
+
const failure = Left<string, number>("invalid input")
|
|
187
|
+
|
|
188
|
+
// Map: transform right value
|
|
189
|
+
const doubled = success.map((x) => x * 2) // Right(10)
|
|
190
|
+
const failureMap = failure.map((x) => x * 2) // Left("invalid input")
|
|
191
|
+
|
|
192
|
+
// MapLeft: transform left value
|
|
193
|
+
const upperError = failure.mapLeft((e) => e.toUpperCase()) // Left("INVALID INPUT")
|
|
194
|
+
const successMapLeft = success.mapLeft((e) => e.toUpperCase()) // Right(5)
|
|
195
|
+
|
|
196
|
+
// Flat map (chain operations)
|
|
197
|
+
const divide = (n: number): Either<string, number> => (n === 0 ? Left("Division by zero") : Right(10 / n))
|
|
198
|
+
|
|
199
|
+
const result1 = success.flatMap(divide) // Right(2)
|
|
200
|
+
const result2 = Right<string, number>(0).flatMap(divide) // Left("Division by zero")
|
|
201
|
+
const result3 = failure.flatMap(divide) // Left("invalid input")
|
|
202
|
+
|
|
203
|
+
// Swap left and right
|
|
204
|
+
const swapped = success.swap() // Left(5)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Pattern Matching
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { Either, Left, Right } from "functype"
|
|
211
|
+
|
|
212
|
+
const success = Right<string, number>(42)
|
|
213
|
+
const failure = Left<string, number>("error")
|
|
214
|
+
|
|
215
|
+
// Using fold for pattern matching
|
|
216
|
+
const result1 = success.fold(
|
|
217
|
+
(left) => `Error: ${left}`,
|
|
218
|
+
(right) => `Success: ${right}`,
|
|
219
|
+
)
|
|
220
|
+
console.log(result1) // "Success: 42"
|
|
221
|
+
|
|
222
|
+
const result2 = failure.fold(
|
|
223
|
+
(left) => `Error: ${left}`,
|
|
224
|
+
(right) => `Success: ${right}`,
|
|
225
|
+
)
|
|
226
|
+
console.log(result2) // "Error: error"
|
|
227
|
+
|
|
228
|
+
// Using match for pattern matching
|
|
229
|
+
const message1 = success.match({
|
|
230
|
+
Right: (value) => `Result: ${value}`,
|
|
231
|
+
Left: (error) => `Failed: ${error}`,
|
|
232
|
+
})
|
|
233
|
+
console.log(message1) // "Result: 42"
|
|
234
|
+
|
|
235
|
+
const message2 = failure.match({
|
|
236
|
+
Right: (value) => `Result: ${value}`,
|
|
237
|
+
Left: (error) => `Failed: ${error}`,
|
|
238
|
+
})
|
|
239
|
+
console.log(message2) // "Failed: error"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Error Handling
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { Either } from "functype"
|
|
246
|
+
|
|
247
|
+
// tryCatch for synchronous operations
|
|
248
|
+
const jsonResult = Either.tryCatch(
|
|
249
|
+
() => JSON.parse('{"name":"John"}'),
|
|
250
|
+
(e) => (e instanceof Error ? e.message : String(e)),
|
|
251
|
+
) // Right({name: "John"})
|
|
252
|
+
|
|
253
|
+
const badJsonResult = Either.tryCatch(
|
|
254
|
+
() => JSON.parse("invalid json"),
|
|
255
|
+
(e) => (e instanceof Error ? e.message : String(e)),
|
|
256
|
+
) // Left("Unexpected token i in JSON at position 0")
|
|
257
|
+
|
|
258
|
+
// tryCatchAsync for asynchronous operations
|
|
259
|
+
async function fetchData() {
|
|
260
|
+
const result = await Either.tryCatchAsync(
|
|
261
|
+
async () => {
|
|
262
|
+
const res = await fetch("https://api.example.com/data")
|
|
263
|
+
if (!res.ok) throw new Error(`HTTP error: ${res.status}`)
|
|
264
|
+
return res.json()
|
|
265
|
+
},
|
|
266
|
+
(e) => (e instanceof Error ? e.message : String(e)),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return result.fold(
|
|
270
|
+
(error) => console.error("Failed to fetch:", error),
|
|
271
|
+
(data) => console.log("Data:", data),
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Factory Methods
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { Either } from "functype"
|
|
280
|
+
|
|
281
|
+
// Create from nullable values
|
|
282
|
+
const fromNull = Either.fromNullable(null, "Value was null") // Left("Value was null")
|
|
283
|
+
const fromValue = Either.fromNullable(42, "Value was null") // Right(42)
|
|
284
|
+
|
|
285
|
+
// Create from try-catch
|
|
286
|
+
const fromTry = Either.fromTry(
|
|
287
|
+
() => JSON.parse('{"name":"John"}'),
|
|
288
|
+
(e) => `Parse error: ${e}`,
|
|
289
|
+
) // Right({name: "John"})
|
|
290
|
+
|
|
291
|
+
// Create from predicate
|
|
292
|
+
const validNumber = Either.fromPredicate(
|
|
293
|
+
42,
|
|
294
|
+
(n) => n > 0,
|
|
295
|
+
(n) => `Number ${n} is not positive`,
|
|
296
|
+
) // Right(42)
|
|
297
|
+
|
|
298
|
+
const invalidNumber = Either.fromPredicate(
|
|
299
|
+
-5,
|
|
300
|
+
(n) => n > 0,
|
|
301
|
+
(n) => `Number ${n} is not positive`,
|
|
302
|
+
) // Left("Number -5 is not positive")
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Try
|
|
306
|
+
|
|
307
|
+
The `Try` type represents a computation that might throw an exception.
|
|
308
|
+
|
|
309
|
+
### Basic Usage
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { Try, Success, Failure } from "functype"
|
|
313
|
+
|
|
314
|
+
// Creating Try values
|
|
315
|
+
const success = Try(() => 42) // Success(42)
|
|
316
|
+
const failure = Try(() => {
|
|
317
|
+
throw new Error("Something went wrong")
|
|
318
|
+
}) // Failure(Error)
|
|
319
|
+
|
|
320
|
+
// Check variants
|
|
321
|
+
console.log(success.isSuccess()) // true
|
|
322
|
+
console.log(success.isFailure()) // false
|
|
323
|
+
console.log(failure.isSuccess()) // false
|
|
324
|
+
console.log(failure.isFailure()) // true
|
|
325
|
+
|
|
326
|
+
// Safe access
|
|
327
|
+
console.log(success.get()) // 42
|
|
328
|
+
// console.log(failure.get()) // Would throw the original error
|
|
329
|
+
|
|
330
|
+
// Default values
|
|
331
|
+
console.log(success.getOrElse(0)) // 42
|
|
332
|
+
console.log(failure.getOrElse(0)) // 0
|
|
333
|
+
|
|
334
|
+
// Access the error
|
|
335
|
+
console.log(failure.error.message) // "Something went wrong"
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Transformations
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { Try } from "functype"
|
|
342
|
+
|
|
343
|
+
const success = Try(() => 5)
|
|
344
|
+
const failure = Try(() => {
|
|
345
|
+
throw new Error("Division error")
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// Map: transform success values
|
|
349
|
+
const doubled = success.map((x) => x * 2) // Success(10)
|
|
350
|
+
const failureMap = failure.map((x) => x * 2) // Failure(Error)
|
|
351
|
+
|
|
352
|
+
// Flat map (chain operations)
|
|
353
|
+
const divide = (n: number) =>
|
|
354
|
+
Try(() => {
|
|
355
|
+
if (n === 0) throw new Error("Division by zero")
|
|
356
|
+
return 10 / n
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const result1 = success.flatMap(divide) // Success(2)
|
|
360
|
+
const result2 = Try(() => 0).flatMap(divide) // Failure(Error: Division by zero)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Pattern Matching
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { Try } from "functype"
|
|
367
|
+
|
|
368
|
+
const success = Try(() => 42)
|
|
369
|
+
const failure = Try(() => {
|
|
370
|
+
throw new Error("Something went wrong")
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// Using fold for pattern matching
|
|
374
|
+
const result1 = success.fold(
|
|
375
|
+
(error) => `Error: ${error.message}`,
|
|
376
|
+
(value) => `Success: ${value}`,
|
|
377
|
+
)
|
|
378
|
+
console.log(result1) // "Success: 42"
|
|
379
|
+
|
|
380
|
+
const result2 = failure.fold(
|
|
381
|
+
(error) => `Error: ${error.message}`,
|
|
382
|
+
(value) => `Success: ${value}`,
|
|
383
|
+
)
|
|
384
|
+
console.log(result2) // "Error: Something went wrong"
|
|
385
|
+
|
|
386
|
+
// Using match for pattern matching
|
|
387
|
+
const message1 = success.match({
|
|
388
|
+
Success: (value) => `Result: ${value}`,
|
|
389
|
+
Failure: (error) => `Failed: ${error.message}`,
|
|
390
|
+
})
|
|
391
|
+
console.log(message1) // "Result: 42"
|
|
392
|
+
|
|
393
|
+
const message2 = failure.match({
|
|
394
|
+
Success: (value) => `Result: ${value}`,
|
|
395
|
+
Failure: (error) => `Failed: ${error.message}`,
|
|
396
|
+
})
|
|
397
|
+
console.log(message2) // "Failed: Something went wrong"
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Recovery
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { Try } from "functype"
|
|
404
|
+
|
|
405
|
+
const failure = Try(() => {
|
|
406
|
+
throw new Error("Network error")
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// Recover with a default value
|
|
410
|
+
const recovered = failure.recover(0) // Success(0)
|
|
411
|
+
|
|
412
|
+
// Recover with another Try
|
|
413
|
+
const recoveredTry = failure.recoverWith(() => Try(() => "Fallback")) // Success("Fallback")
|
|
414
|
+
|
|
415
|
+
// Convert to other types
|
|
416
|
+
const asEither = failure.toEither() // Left(Error: Network error)
|
|
417
|
+
const asOption = failure.toOption() // None
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## List
|
|
421
|
+
|
|
422
|
+
The `List` type is an immutable list with functional operations.
|
|
423
|
+
|
|
424
|
+
### Basic Usage
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { List } from "functype"
|
|
428
|
+
|
|
429
|
+
// Creating lists
|
|
430
|
+
const empty = List([]) // List([])
|
|
431
|
+
const numbers = List([1, 2, 3, 4, 5]) // List([1, 2, 3, 4, 5])
|
|
432
|
+
const mixed = List([1, "two", true]) // List([1, "two", true])
|
|
433
|
+
|
|
434
|
+
// Access elements
|
|
435
|
+
console.log(numbers.head()) // Option(1)
|
|
436
|
+
console.log(numbers.tail()) // List([2, 3, 4, 5])
|
|
437
|
+
console.log(empty.head()) // None
|
|
438
|
+
|
|
439
|
+
// Check properties
|
|
440
|
+
console.log(numbers.isEmpty()) // false
|
|
441
|
+
console.log(empty.isEmpty()) // true
|
|
442
|
+
console.log(numbers.size()) // 5
|
|
443
|
+
|
|
444
|
+
// Add/remove elements (immutably)
|
|
445
|
+
const withSix = numbers.add(6) // List([1, 2, 3, 4, 5, 6])
|
|
446
|
+
const without3 = numbers.remove(3) // List([1, 2, 4, 5])
|
|
447
|
+
|
|
448
|
+
// Convert to array
|
|
449
|
+
console.log(numbers.toArray()) // [1, 2, 3, 4, 5]
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Transformations
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import { List } from "functype"
|
|
456
|
+
|
|
457
|
+
const numbers = List([1, 2, 3, 4, 5])
|
|
458
|
+
|
|
459
|
+
// Map: transform each element
|
|
460
|
+
const doubled = numbers.map((x) => x * 2) // List([2, 4, 6, 8, 10])
|
|
461
|
+
|
|
462
|
+
// Filter: keep elements matching predicate
|
|
463
|
+
const evens = numbers.filter((x) => x % 2 === 0) // List([2, 4])
|
|
464
|
+
|
|
465
|
+
// FlatMap: transform and flatten
|
|
466
|
+
const pairs = numbers.flatMap((x) => List([x, x])) // List([1, 1, 2, 2, 3, 3, 4, 4, 5, 5])
|
|
467
|
+
|
|
468
|
+
// Take/Drop
|
|
469
|
+
const firstThree = numbers.take(3) // List([1, 2, 3])
|
|
470
|
+
const lastTwo = numbers.drop(3) // List([4, 5])
|
|
471
|
+
|
|
472
|
+
// Slicing
|
|
473
|
+
const middle = numbers.slice(1, 4) // List([2, 3, 4])
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Aggregations
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import { List } from "functype"
|
|
480
|
+
|
|
481
|
+
const numbers = List([1, 2, 3, 4, 5])
|
|
482
|
+
|
|
483
|
+
// Reduce (foldLeft)
|
|
484
|
+
const sum = numbers.foldLeft(0)((acc, x) => acc + x) // 15
|
|
485
|
+
|
|
486
|
+
// Right-associative fold
|
|
487
|
+
const sumRight = numbers.foldRight(0)((x, acc) => x + acc) // 15
|
|
488
|
+
|
|
489
|
+
// Find elements
|
|
490
|
+
const firstEven = numbers.find((x) => x % 2 === 0) // Some(2)
|
|
491
|
+
const noMatch = numbers.find((x) => x > 10) // None
|
|
492
|
+
|
|
493
|
+
// Check if elements exist
|
|
494
|
+
console.log(numbers.exists((x) => x === 3)) // true
|
|
495
|
+
console.log(numbers.forAll((x) => x < 10)) // true
|
|
496
|
+
|
|
497
|
+
// Count elements
|
|
498
|
+
console.log(numbers.count((x) => x % 2 === 0)) // 2
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Operations
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
import { List } from "functype"
|
|
505
|
+
|
|
506
|
+
const list1 = List([1, 2, 3])
|
|
507
|
+
const list2 = List([4, 5, 6])
|
|
508
|
+
|
|
509
|
+
// Concatenation
|
|
510
|
+
const combined = list1.concat(list2) // List([1, 2, 3, 4, 5, 6])
|
|
511
|
+
|
|
512
|
+
// Reverse
|
|
513
|
+
const reversed = list1.reverse() // List([3, 2, 1])
|
|
514
|
+
|
|
515
|
+
// Sort
|
|
516
|
+
const unsorted = List([3, 1, 4, 2, 5])
|
|
517
|
+
const sorted = unsorted.sort((a, b) => a - b) // List([1, 2, 3, 4, 5])
|
|
518
|
+
|
|
519
|
+
// Unique values
|
|
520
|
+
const duplicates = List([1, 2, 2, 3, 3, 3])
|
|
521
|
+
const unique = duplicates.distinct() // List([1, 2, 3])
|
|
522
|
+
|
|
523
|
+
// Grouping
|
|
524
|
+
const grouped = List([1, 2, 3, 4, 5, 6]).groupBy((x) => (x % 2 === 0 ? "even" : "odd"))
|
|
525
|
+
// Map({ 'odd': List([1, 3, 5]), 'even': List([2, 4, 6]) })
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Pattern Matching
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { List } from "functype"
|
|
532
|
+
|
|
533
|
+
const numbers = List([1, 2, 3])
|
|
534
|
+
const empty = List([])
|
|
535
|
+
|
|
536
|
+
// Using match for pattern matching
|
|
537
|
+
const result1 = numbers.match({
|
|
538
|
+
NonEmpty: (values) => `Values: ${values.join(", ")}`,
|
|
539
|
+
Empty: () => "No values",
|
|
540
|
+
})
|
|
541
|
+
console.log(result1) // "Values: 1, 2, 3"
|
|
542
|
+
|
|
543
|
+
const result2 = empty.match({
|
|
544
|
+
NonEmpty: (values) => `Values: ${values.join(", ")}`,
|
|
545
|
+
Empty: () => "No values",
|
|
546
|
+
})
|
|
547
|
+
console.log(result2) // "No values"
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## Map
|
|
551
|
+
|
|
552
|
+
The `Map` type is an immutable key-value map with functional operations.
|
|
553
|
+
|
|
554
|
+
### Basic Usage
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import { Map } from "functype"
|
|
558
|
+
|
|
559
|
+
// Creating maps
|
|
560
|
+
const empty = Map<string, number>({})
|
|
561
|
+
const scores = Map({
|
|
562
|
+
alice: 95,
|
|
563
|
+
bob: 87,
|
|
564
|
+
charlie: 92,
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
// Access elements
|
|
568
|
+
console.log(scores.get("alice")) // Some(95)
|
|
569
|
+
console.log(scores.get("dave")) // None
|
|
570
|
+
console.log(scores.getOrElse("dave", 0)) // 0
|
|
571
|
+
|
|
572
|
+
// Check properties
|
|
573
|
+
console.log(scores.isEmpty()) // false
|
|
574
|
+
console.log(empty.isEmpty()) // true
|
|
575
|
+
console.log(scores.size()) // 3
|
|
576
|
+
console.log(scores.has("bob")) // true
|
|
577
|
+
|
|
578
|
+
// Keys and values
|
|
579
|
+
console.log(scores.keys()) // List(["alice", "bob", "charlie"])
|
|
580
|
+
console.log(scores.values()) // List([95, 87, 92])
|
|
581
|
+
|
|
582
|
+
// Add/remove entries (immutably)
|
|
583
|
+
const withDave = scores.add("dave", 83) // Map with dave added
|
|
584
|
+
const withoutBob = scores.remove("bob") // Map without bob
|
|
585
|
+
|
|
586
|
+
// Convert to object
|
|
587
|
+
console.log(scores.toObject()) // { alice: 95, bob: 87, charlie: 92 }
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Transformations
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import { Map } from "functype"
|
|
594
|
+
|
|
595
|
+
const scores = Map({
|
|
596
|
+
alice: 95,
|
|
597
|
+
bob: 87,
|
|
598
|
+
charlie: 92,
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// Map: transform values
|
|
602
|
+
const grades = scores.map((score) => {
|
|
603
|
+
if (score >= 90) return "A"
|
|
604
|
+
if (score >= 80) return "B"
|
|
605
|
+
return "C"
|
|
606
|
+
})
|
|
607
|
+
// Map({ alice: 'A', bob: 'B', charlie: 'A' })
|
|
608
|
+
|
|
609
|
+
// Filter entries
|
|
610
|
+
const highScores = scores.filter((score, key) => score >= 90)
|
|
611
|
+
// Map({ alice: 95, charlie: 92 })
|
|
612
|
+
|
|
613
|
+
// Map entries (both key and value)
|
|
614
|
+
const prefixed = scores.mapEntries(([key, value]) => [`student_${key}`, value])
|
|
615
|
+
// Map({ student_alice: 95, student_bob: 87, student_charlie: 92 })
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Aggregations
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
import { Map } from "functype"
|
|
622
|
+
|
|
623
|
+
const scores = Map({
|
|
624
|
+
alice: 95,
|
|
625
|
+
bob: 87,
|
|
626
|
+
charlie: 92,
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
// Fold
|
|
630
|
+
const total = scores.foldLeft(0)((acc, score) => acc + score) // 274
|
|
631
|
+
const report = scores.foldLeft("")((acc, score, name) => `${acc}${name}: ${score}\n`)
|
|
632
|
+
// "alice: 95\nbob: 87\ncharlie: 92\n"
|
|
633
|
+
|
|
634
|
+
// Find entries
|
|
635
|
+
const found = scores.find((score) => score > 90) // Some([alice, 95])
|
|
636
|
+
const notFound = scores.find((score) => score > 100) // None
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Operations
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
import { Map } from "functype"
|
|
643
|
+
|
|
644
|
+
const group1 = Map({ alice: 95, bob: 87 })
|
|
645
|
+
const group2 = Map({ charlie: 92, dave: 88 })
|
|
646
|
+
|
|
647
|
+
// Merge maps
|
|
648
|
+
const allScores = group1.merge(group2)
|
|
649
|
+
// Map({ alice: 95, bob: 87, charlie: 92, dave: 88 })
|
|
650
|
+
|
|
651
|
+
// Override values
|
|
652
|
+
const updated = group1.merge(Map({ bob: 90 }))
|
|
653
|
+
// Map({ alice: 95, bob: 90 })
|
|
654
|
+
|
|
655
|
+
// Custom merge logic
|
|
656
|
+
const merged = group1.mergeWith(Map({ bob: 90 }), (v1, v2) => Math.max(v1, v2))
|
|
657
|
+
// Map({ alice: 95, bob: 90 })
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Set
|
|
661
|
+
|
|
662
|
+
The `Set` type is an immutable set collection with functional operations.
|
|
663
|
+
|
|
664
|
+
### Basic Usage
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
import { Set } from "functype"
|
|
668
|
+
|
|
669
|
+
// Creating sets
|
|
670
|
+
const empty = Set<number>([])
|
|
671
|
+
const numbers = Set([1, 2, 3, 4, 5])
|
|
672
|
+
const withDuplicates = Set([1, 1, 2, 2, 3]) // Set([1, 2, 3])
|
|
673
|
+
|
|
674
|
+
// Check properties
|
|
675
|
+
console.log(numbers.isEmpty()) // false
|
|
676
|
+
console.log(empty.isEmpty()) // true
|
|
677
|
+
console.log(numbers.size()) // 5
|
|
678
|
+
console.log(numbers.has(3)) // true
|
|
679
|
+
console.log(numbers.has(10)) // false
|
|
680
|
+
|
|
681
|
+
// Add/remove elements (immutably)
|
|
682
|
+
const withSix = numbers.add(6) // Set([1, 2, 3, 4, 5, 6])
|
|
683
|
+
const without3 = numbers.remove(3) // Set([1, 2, 4, 5])
|
|
684
|
+
|
|
685
|
+
// Convert to array
|
|
686
|
+
console.log(numbers.toArray()) // [1, 2, 3, 4, 5]
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Transformations
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
import { Set } from "functype"
|
|
693
|
+
|
|
694
|
+
const numbers = Set([1, 2, 3, 4, 5])
|
|
695
|
+
|
|
696
|
+
// Map: transform each element
|
|
697
|
+
const doubled = numbers.map((x) => x * 2) // Set([2, 4, 6, 8, 10])
|
|
698
|
+
|
|
699
|
+
// Filter: keep elements matching predicate
|
|
700
|
+
const evens = numbers.filter((x) => x % 2 === 0) // Set([2, 4])
|
|
701
|
+
|
|
702
|
+
// FlatMap: transform and flatten
|
|
703
|
+
const adjacents = numbers.flatMap((x) => Set([x - 1, x, x + 1]))
|
|
704
|
+
// Set([0, 1, 2, 3, 4, 5, 6])
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Set Operations
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
import { Set } from "functype"
|
|
711
|
+
|
|
712
|
+
const set1 = Set([1, 2, 3, 4])
|
|
713
|
+
const set2 = Set([3, 4, 5, 6])
|
|
714
|
+
|
|
715
|
+
// Union
|
|
716
|
+
const union = set1.union(set2) // Set([1, 2, 3, 4, 5, 6])
|
|
717
|
+
|
|
718
|
+
// Intersection
|
|
719
|
+
const intersection = set1.intersect(set2) // Set([3, 4])
|
|
720
|
+
|
|
721
|
+
// Difference
|
|
722
|
+
const difference = set1.difference(set2) // Set([1, 2])
|
|
723
|
+
|
|
724
|
+
// Symmetric difference
|
|
725
|
+
const symmetricDiff = set1.symmetricDifference(set2) // Set([1, 2, 5, 6])
|
|
726
|
+
|
|
727
|
+
// Subset check
|
|
728
|
+
const subset = Set([1, 2])
|
|
729
|
+
console.log(subset.isSubsetOf(set1)) // true
|
|
730
|
+
console.log(set1.isSubsetOf(subset)) // false
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Aggregations
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
import { Set } from "functype"
|
|
737
|
+
|
|
738
|
+
const numbers = Set([1, 2, 3, 4, 5])
|
|
739
|
+
|
|
740
|
+
// Fold
|
|
741
|
+
const sum = numbers.foldLeft(0)((acc, x) => acc + x) // 15
|
|
742
|
+
|
|
743
|
+
// Find elements
|
|
744
|
+
const firstEven = numbers.find((x) => x % 2 === 0) // Some(2)
|
|
745
|
+
const noMatch = numbers.find((x) => x > 10) // None
|
|
746
|
+
|
|
747
|
+
// Check if elements exist
|
|
748
|
+
console.log(numbers.exists((x) => x % 2 === 0)) // true
|
|
749
|
+
console.log(numbers.forAll((x) => x < 10)) // true
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
## FPromise
|
|
753
|
+
|
|
754
|
+
The `FPromise` type enhances JavaScript's Promise with functional operations and better error handling.
|
|
755
|
+
|
|
756
|
+
### Basic Usage
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
import { FPromise } from "functype"
|
|
760
|
+
|
|
761
|
+
// Creating FPromises
|
|
762
|
+
const success = FPromise.resolve(42)
|
|
763
|
+
const failure = FPromise.reject(new Error("Something went wrong"))
|
|
764
|
+
const fromPromise = FPromise.fromPromise(fetch("https://api.example.com/data"))
|
|
765
|
+
|
|
766
|
+
// From regular functions
|
|
767
|
+
const compute = () => 42
|
|
768
|
+
const fp1 = FPromise.tryCatch(compute) // FPromise<number, Error>
|
|
769
|
+
|
|
770
|
+
// From async functions
|
|
771
|
+
const fetchData = async () => {
|
|
772
|
+
const response = await fetch("https://api.example.com/data")
|
|
773
|
+
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
|
|
774
|
+
return response.json()
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const fp2 = FPromise.tryCatch(fetchData) // FPromise<Data, Error>
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Transformations
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
import { FPromise } from "functype"
|
|
784
|
+
|
|
785
|
+
const promise = FPromise.resolve(5)
|
|
786
|
+
|
|
787
|
+
// Map: transform success value
|
|
788
|
+
const doubled = promise.map((x) => x * 2) // FPromise<10, never>
|
|
789
|
+
|
|
790
|
+
// MapError: transform error value
|
|
791
|
+
const mappedError = promise.mapError((e) => new Error(`Enhanced error: ${e.message}`))
|
|
792
|
+
|
|
793
|
+
// FlatMap: chain operations
|
|
794
|
+
const nextOperation = (n: number) => FPromise.resolve(n.toString())
|
|
795
|
+
const chained = promise.flatMap(nextOperation) // FPromise<"5", never>
|
|
796
|
+
|
|
797
|
+
// Tap: perform side effects
|
|
798
|
+
const withLogging = promise.tap((value) => {
|
|
799
|
+
console.log("Processing value:", value)
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
// TapError: side effects for errors
|
|
803
|
+
const withErrorLogging = promise.tapError((error) => {
|
|
804
|
+
console.error("Error occurred:", error)
|
|
805
|
+
})
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Error Handling
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
import { FPromise } from "functype"
|
|
812
|
+
|
|
813
|
+
const failedPromise = FPromise.reject(new Error("Network error"))
|
|
814
|
+
|
|
815
|
+
// Recover with a default value
|
|
816
|
+
const recovered = failedPromise.recover(0) // FPromise<0, never>
|
|
817
|
+
|
|
818
|
+
// Recover with another operation
|
|
819
|
+
const recoveredWith = failedPromise.recoverWith((err) => FPromise.resolve(`Recovered from: ${err.message}`))
|
|
820
|
+
|
|
821
|
+
// Handle both success and error paths
|
|
822
|
+
const handled = FPromise.resolve(42).fold(
|
|
823
|
+
(err) => `Error: ${err.message}`,
|
|
824
|
+
(value) => `Success: ${value}`,
|
|
825
|
+
) // FPromise<"Success: 42", never>
|
|
826
|
+
|
|
827
|
+
// Convert to standard Promise
|
|
828
|
+
const stdPromise = FPromise.resolve(42).toPromise()
|
|
829
|
+
stdPromise.then((value) => console.log(value)) // 42
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Parallel Operations
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
import { FPromise } from "functype"
|
|
836
|
+
|
|
837
|
+
const p1 = FPromise.resolve(1)
|
|
838
|
+
const p2 = FPromise.resolve(2)
|
|
839
|
+
const p3 = FPromise.resolve(3)
|
|
840
|
+
|
|
841
|
+
// Parallel execution
|
|
842
|
+
const all = FPromise.all([p1, p2, p3]) // FPromise<[1, 2, 3], never>
|
|
843
|
+
|
|
844
|
+
// Race
|
|
845
|
+
const race = FPromise.race([FPromise.delay(100).map(() => "fast"), FPromise.delay(200).map(() => "slow")]) // FPromise<"fast", never>
|
|
846
|
+
|
|
847
|
+
// With timeout
|
|
848
|
+
const withTimeout = FPromise.timeout(
|
|
849
|
+
FPromise.delay(2000).map(() => "result"),
|
|
850
|
+
1000,
|
|
851
|
+
() => new Error("Operation timed out"),
|
|
852
|
+
) // FPromise<never, Error> (times out)
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Retry Logic
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
import { FPromise, retry } from "functype/fpromise"
|
|
859
|
+
|
|
860
|
+
const unreliableOperation = () => {
|
|
861
|
+
// Simulate an operation that sometimes fails
|
|
862
|
+
if (Math.random() < 0.7) {
|
|
863
|
+
return FPromise.reject(new Error("Temporary failure"))
|
|
864
|
+
}
|
|
865
|
+
return FPromise.resolve("Success!")
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Basic retry
|
|
869
|
+
const retried = retry({
|
|
870
|
+
task: unreliableOperation,
|
|
871
|
+
maxRetries: 5,
|
|
872
|
+
}) // Will retry up to 5 times
|
|
873
|
+
|
|
874
|
+
// Advanced retry with backoff
|
|
875
|
+
const retriedWithBackoff = retry({
|
|
876
|
+
task: unreliableOperation,
|
|
877
|
+
maxRetries: 5,
|
|
878
|
+
delay: 1000, // Start with 1 second delay
|
|
879
|
+
backoffFactor: 2, // Double delay after each attempt
|
|
880
|
+
maxDelay: 10000, // Cap delay at 10 seconds
|
|
881
|
+
retryIf: (error) => error.message.includes("Temporary"), // Only retry certain errors
|
|
882
|
+
})
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
## Task
|
|
886
|
+
|
|
887
|
+
The `Task` type represents synchronous and asynchronous operations with error handling.
|
|
888
|
+
|
|
889
|
+
### Basic Usage
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
import { Task } from "functype"
|
|
893
|
+
|
|
894
|
+
// Synchronous tasks
|
|
895
|
+
const syncTask = Task().Sync(
|
|
896
|
+
() => 42, // Success function
|
|
897
|
+
(error) => new Error(`Failed: ${error}`), // Error function
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
// Asynchronous tasks
|
|
901
|
+
const asyncTask = Task().Async(
|
|
902
|
+
async () => {
|
|
903
|
+
const response = await fetch("https://api.example.com/data")
|
|
904
|
+
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
|
|
905
|
+
return response.json()
|
|
906
|
+
},
|
|
907
|
+
async (error) => new Error(`Fetch failed: ${error}`),
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
// Named tasks (for debugging)
|
|
911
|
+
const namedTask = Task({ name: "UserFetch" }).Sync(
|
|
912
|
+
() => ({ id: 1, name: "John Doe" }),
|
|
913
|
+
(error) => new Error(`User fetch failed: ${error}`),
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
// Running tasks
|
|
917
|
+
syncTask.then((value) => console.log("Success:", value)).catch((error) => console.error("Error:", error))
|
|
918
|
+
|
|
919
|
+
// With async/await
|
|
920
|
+
async function runTask() {
|
|
921
|
+
try {
|
|
922
|
+
const result = await asyncTask
|
|
923
|
+
console.log("Data:", result)
|
|
924
|
+
} catch (error) {
|
|
925
|
+
console.error("Failed:", error)
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### Adapting External APIs
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
import { Task } from "functype"
|
|
934
|
+
|
|
935
|
+
// Convert promise-based APIs to Task
|
|
936
|
+
const fetchUser = (id: string): Promise<User> => fetch(`/api/users/${id}`).then((r) => r.json())
|
|
937
|
+
|
|
938
|
+
// Create a Task adapter
|
|
939
|
+
const getUser = Task({ name: "UserFetch" }).fromPromise(fetchUser)
|
|
940
|
+
|
|
941
|
+
// Use the task
|
|
942
|
+
getUser("user123")
|
|
943
|
+
.then((user) => console.log(user))
|
|
944
|
+
.catch((error) => console.error(error))
|
|
945
|
+
|
|
946
|
+
// Convert back to Promise when needed
|
|
947
|
+
const task = Task().Sync(() => "hello world")
|
|
948
|
+
const promise = Task().toPromise(task) // Promise<string>
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
### Composition
|
|
952
|
+
|
|
953
|
+
```typescript
|
|
954
|
+
import { Task } from "functype"
|
|
955
|
+
|
|
956
|
+
// Define component tasks
|
|
957
|
+
const fetchUser = Task({ name: "FetchUser" }).Async(
|
|
958
|
+
async (userId: string) => {
|
|
959
|
+
const response = await fetch(`/api/users/${userId}`)
|
|
960
|
+
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
|
|
961
|
+
return response.json()
|
|
962
|
+
},
|
|
963
|
+
async (error) => new Error(`User fetch failed: ${error}`),
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
const fetchPosts = Task({ name: "FetchPosts" }).Async(
|
|
967
|
+
async (userId: string) => {
|
|
968
|
+
const response = await fetch(`/api/users/${userId}/posts`)
|
|
969
|
+
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
|
|
970
|
+
return response.json()
|
|
971
|
+
},
|
|
972
|
+
async (error) => new Error(`Posts fetch failed: ${error}`),
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
// Compose tasks
|
|
976
|
+
async function getUserWithPosts(userId: string) {
|
|
977
|
+
try {
|
|
978
|
+
// Run tasks in sequence with dependencies
|
|
979
|
+
const user = await fetchUser(userId)
|
|
980
|
+
const posts = await fetchPosts(userId)
|
|
981
|
+
return { user, posts }
|
|
982
|
+
} catch (error) {
|
|
983
|
+
console.error("Error:", error)
|
|
984
|
+
throw error
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Run parallel tasks
|
|
989
|
+
async function getMultipleUsers(userIds: string[]) {
|
|
990
|
+
try {
|
|
991
|
+
const userTasks = userIds.map((id) => fetchUser(id))
|
|
992
|
+
const users = await Promise.all(userTasks)
|
|
993
|
+
return users
|
|
994
|
+
} catch (error) {
|
|
995
|
+
console.error("Error:", error)
|
|
996
|
+
throw error
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
## Branded Types
|
|
1002
|
+
|
|
1003
|
+
Branded types provide nominal typing in TypeScript's structural type system, giving stronger type safety.
|
|
1004
|
+
|
|
1005
|
+
### Basic Usage
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
import { Brand } from "functype/branded"
|
|
1009
|
+
|
|
1010
|
+
// Create branded types
|
|
1011
|
+
type UserId = Brand<string, "UserId">
|
|
1012
|
+
type Email = Brand<string, "Email">
|
|
1013
|
+
|
|
1014
|
+
// Create factory functions with validation
|
|
1015
|
+
const UserId = (id: string): UserId => {
|
|
1016
|
+
if (!/^U\d{6}$/.test(id)) {
|
|
1017
|
+
throw new Error("Invalid user ID format")
|
|
1018
|
+
}
|
|
1019
|
+
return id as UserId
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const Email = (email: string): Email => {
|
|
1023
|
+
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) {
|
|
1024
|
+
throw new Error("Invalid email format")
|
|
1025
|
+
}
|
|
1026
|
+
return email as Email
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Usage
|
|
1030
|
+
function getUserByEmail(email: Email): User {
|
|
1031
|
+
/* ... */
|
|
1032
|
+
return { id: "U123456" as UserId, email }
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Type safety in action
|
|
1036
|
+
const email = Email("user@example.com")
|
|
1037
|
+
const user = getUserByEmail(email) // Works
|
|
1038
|
+
|
|
1039
|
+
// These would cause type errors
|
|
1040
|
+
// getUserByEmail("invalid") // Error: Argument of type 'string' is not assignable to parameter of type 'Email'
|
|
1041
|
+
// getUserByEmail(UserId("U123456")) // Error: Argument of type 'UserId' is not assignable to parameter of type 'Email'
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### Advanced Branded Types
|
|
1045
|
+
|
|
1046
|
+
```typescript
|
|
1047
|
+
import { Brand } from "functype/branded"
|
|
1048
|
+
|
|
1049
|
+
// Numeric constraints
|
|
1050
|
+
type PositiveInt = Brand<number, "PositiveInt">
|
|
1051
|
+
type Percentage = Brand<number, "Percentage">
|
|
1052
|
+
|
|
1053
|
+
const PositiveInt = (n: number): PositiveInt => {
|
|
1054
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
1055
|
+
throw new Error("Must be a positive integer")
|
|
1056
|
+
}
|
|
1057
|
+
return n as PositiveInt
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const Percentage = (n: number): Percentage => {
|
|
1061
|
+
if (n < 0 || n > 100) {
|
|
1062
|
+
throw new Error("Percentage must be between 0 and 100")
|
|
1063
|
+
}
|
|
1064
|
+
return n as Percentage
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// String format validation
|
|
1068
|
+
type ISBN = Brand<string, "ISBN">
|
|
1069
|
+
type CreditCardNumber = Brand<string, "CreditCardNumber">
|
|
1070
|
+
|
|
1071
|
+
const ISBN = (isbn: string): ISBN => {
|
|
1072
|
+
// Simplified validation
|
|
1073
|
+
if (!/^\d{10}(\d{3})?$/.test(isbn.replace(/-/g, ""))) {
|
|
1074
|
+
throw new Error("Invalid ISBN format")
|
|
1075
|
+
}
|
|
1076
|
+
return isbn as ISBN
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Combining with Option for safe creation
|
|
1080
|
+
import { Option } from "functype/option"
|
|
1081
|
+
|
|
1082
|
+
const SafeEmail = (email: string): Option<Email> => {
|
|
1083
|
+
try {
|
|
1084
|
+
return Option(Email(email))
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
return Option(null)
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Usage
|
|
1091
|
+
const maybeEmail = SafeEmail("invalid") // None
|
|
1092
|
+
const validEmail = SafeEmail("user@example.com") // Some(Email)
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
## Tuple
|
|
1096
|
+
|
|
1097
|
+
The `Tuple` type provides a type-safe fixed-length array.
|
|
1098
|
+
|
|
1099
|
+
### Basic Usage
|
|
1100
|
+
|
|
1101
|
+
```typescript
|
|
1102
|
+
import { Tuple } from "functype"
|
|
1103
|
+
|
|
1104
|
+
// Create tuples of different sizes and types
|
|
1105
|
+
const pair = Tuple(42, "hello") // Tuple<[number, string]>
|
|
1106
|
+
const triple = Tuple(true, 100, "world") // Tuple<[boolean, number, string]>
|
|
1107
|
+
|
|
1108
|
+
// Access elements (type-safe)
|
|
1109
|
+
console.log(pair.first()) // 42
|
|
1110
|
+
console.log(pair.second()) // "hello"
|
|
1111
|
+
console.log(triple.third()) // "world"
|
|
1112
|
+
|
|
1113
|
+
// Destructuring
|
|
1114
|
+
const [a, b] = pair.toArray() // a: number, b: string
|
|
1115
|
+
console.log(a, b) // 42 "hello"
|
|
1116
|
+
|
|
1117
|
+
// Map individual elements
|
|
1118
|
+
const mappedPair = pair.mapFirst((n) => n * 2) // Tuple(84, "hello")
|
|
1119
|
+
const mappedPair2 = pair.mapSecond((s) => s.toUpperCase()) // Tuple(42, "HELLO")
|
|
1120
|
+
|
|
1121
|
+
// Map the entire tuple
|
|
1122
|
+
const mapped = pair.map(([num, str]) => {
|
|
1123
|
+
return [num * 2, str.toUpperCase()] as [number, string]
|
|
1124
|
+
}) // Tuple(84, "HELLO")
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
### Operations
|
|
1128
|
+
|
|
1129
|
+
```typescript
|
|
1130
|
+
import { Tuple } from "functype"
|
|
1131
|
+
|
|
1132
|
+
// Swap elements
|
|
1133
|
+
const pair = Tuple("first", "second")
|
|
1134
|
+
const swapped = pair.swap() // Tuple("second", "first")
|
|
1135
|
+
|
|
1136
|
+
// Apply a function to tuple elements
|
|
1137
|
+
const nums = Tuple(5, 10)
|
|
1138
|
+
const sum = nums.apply((a, b) => a + b) // 15
|
|
1139
|
+
|
|
1140
|
+
// Combine tuples
|
|
1141
|
+
const t1 = Tuple(1, "a")
|
|
1142
|
+
const t2 = Tuple(true, 42)
|
|
1143
|
+
const combined = t1.concat(t2) // Tuple(1, "a", true, 42)
|
|
1144
|
+
|
|
1145
|
+
// Convert to object with keys
|
|
1146
|
+
const person = Tuple("John", 30)
|
|
1147
|
+
const obj = person.toObject(["name", "age"]) // { name: "John", age: 30 }
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
### With Other Types
|
|
1151
|
+
|
|
1152
|
+
```typescript
|
|
1153
|
+
import { Tuple, Option, Either } from "functype"
|
|
1154
|
+
|
|
1155
|
+
// Create tuples with Options
|
|
1156
|
+
const maybePair = Tuple(Option(42), Option("hello"))
|
|
1157
|
+
|
|
1158
|
+
// Map with Options
|
|
1159
|
+
const optResult = maybePair.mapFirst((opt) => opt.map((n) => n * 2))
|
|
1160
|
+
|
|
1161
|
+
// Create tuples with Either
|
|
1162
|
+
const validationPair = Tuple(
|
|
1163
|
+
Either.fromNullable(42, "Missing first value"),
|
|
1164
|
+
Either.fromNullable(null, "Missing second value"),
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
// Check if all Either values are valid
|
|
1168
|
+
const allValid = validationPair.toArray().every((either) => either.isRight()) // false
|
|
1169
|
+
|
|
1170
|
+
// Combine Tuple and Task
|
|
1171
|
+
import { Task } from "functype"
|
|
1172
|
+
|
|
1173
|
+
const taskPair = Tuple(
|
|
1174
|
+
Task().Sync(() => "hello"),
|
|
1175
|
+
Task().Sync(() => 42),
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
// Run tasks in parallel
|
|
1179
|
+
async function runBoth() {
|
|
1180
|
+
const [str, num] = await Promise.all(taskPair.toArray())
|
|
1181
|
+
console.log(str, num) // "hello" 42
|
|
1182
|
+
}
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
## Foldable
|
|
1186
|
+
|
|
1187
|
+
The `Foldable` type class provides a common interface for data structures that can be "folded" to a single value.
|
|
1188
|
+
|
|
1189
|
+
### Using Foldable Interface
|
|
1190
|
+
|
|
1191
|
+
```typescript
|
|
1192
|
+
import { FoldableUtils, Option, List, Try } from "functype"
|
|
1193
|
+
|
|
1194
|
+
// Different data structures implementing Foldable
|
|
1195
|
+
const option = Option(5)
|
|
1196
|
+
const list = List([1, 2, 3, 4, 5])
|
|
1197
|
+
const tryVal = Try(() => 10)
|
|
1198
|
+
|
|
1199
|
+
// Using fold to pattern-match on data structures
|
|
1200
|
+
option.fold(
|
|
1201
|
+
() => console.log("Empty option"),
|
|
1202
|
+
(value) => console.log(`Option value: ${value}`),
|
|
1203
|
+
) // "Option value: 5"
|
|
1204
|
+
|
|
1205
|
+
// Left-associative fold (reduce from left to right)
|
|
1206
|
+
const sum = list.foldLeft(0)((acc, value) => acc + value) // 15
|
|
1207
|
+
|
|
1208
|
+
// Right-associative fold (reduce from right to left)
|
|
1209
|
+
const product = list.foldRight(1)((value, acc) => value * acc) // 120
|
|
1210
|
+
|
|
1211
|
+
// Using FoldableUtils to work with any Foldable
|
|
1212
|
+
const isEmpty = FoldableUtils.isEmpty(option) // false
|
|
1213
|
+
const size = FoldableUtils.size(list) // 5
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
### Converting Between Types
|
|
1217
|
+
|
|
1218
|
+
```typescript
|
|
1219
|
+
import { FoldableUtils, Option, List, Either, Try } from "functype"
|
|
1220
|
+
|
|
1221
|
+
// Convert between data structure types
|
|
1222
|
+
const opt = Option(42)
|
|
1223
|
+
const list = List([1, 2, 3])
|
|
1224
|
+
const either = Either.right<string, number>(10)
|
|
1225
|
+
const tryVal = Try(() => "hello")
|
|
1226
|
+
|
|
1227
|
+
// Convert to List
|
|
1228
|
+
const optAsList = FoldableUtils.toList(opt) // List([42])
|
|
1229
|
+
const eitherAsList = FoldableUtils.toList(either) // List([10])
|
|
1230
|
+
|
|
1231
|
+
// Convert to Option (takes first element from collections)
|
|
1232
|
+
const listAsOption = FoldableUtils.toOption(list) // Some(1)
|
|
1233
|
+
|
|
1234
|
+
// Convert to Either
|
|
1235
|
+
const optAsEither = FoldableUtils.toEither(opt, "Empty") // Right(42)
|
|
1236
|
+
const tryAsEither = FoldableUtils.toEither(tryVal, "Failed") // Right("hello")
|
|
1237
|
+
|
|
1238
|
+
// Extract all values from foldable structures
|
|
1239
|
+
const listValues = FoldableUtils.values(list) // [1, 2, 3]
|
|
1240
|
+
const optValues = FoldableUtils.values(opt) // [42]
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
## Matchable
|
|
1244
|
+
|
|
1245
|
+
The `Matchable` type class provides pattern matching capabilities for data structures.
|
|
1246
|
+
|
|
1247
|
+
### Basic Pattern Matching
|
|
1248
|
+
|
|
1249
|
+
```typescript
|
|
1250
|
+
import { Option, Either, Try, List } from "functype"
|
|
1251
|
+
|
|
1252
|
+
// Pattern matching on Option
|
|
1253
|
+
const opt = Option(42)
|
|
1254
|
+
const optResult = opt.match({
|
|
1255
|
+
Some: (value) => `Found: ${value}`,
|
|
1256
|
+
None: () => "Not found",
|
|
1257
|
+
})
|
|
1258
|
+
console.log(optResult) // "Found: 42"
|
|
1259
|
+
|
|
1260
|
+
// Pattern matching on Either
|
|
1261
|
+
const either = Either.fromNullable(null, "Missing value")
|
|
1262
|
+
const eitherResult = either.match({
|
|
1263
|
+
Left: (error) => `Error: ${error}`,
|
|
1264
|
+
Right: (value) => `Value: ${value}`,
|
|
1265
|
+
})
|
|
1266
|
+
console.log(eitherResult) // "Error: Missing value"
|
|
1267
|
+
|
|
1268
|
+
// Pattern matching on Try
|
|
1269
|
+
const tryVal = Try(() => JSON.parse('{"name":"John"}'))
|
|
1270
|
+
const tryResult = tryVal.match({
|
|
1271
|
+
Success: (data) => `Name: ${data.name}`,
|
|
1272
|
+
Failure: (error) => `Parse error: ${error.message}`,
|
|
1273
|
+
})
|
|
1274
|
+
console.log(tryResult) // "Name: John"
|
|
1275
|
+
|
|
1276
|
+
// Pattern matching on List
|
|
1277
|
+
const list = List([1, 2, 3])
|
|
1278
|
+
const listResult = list.match({
|
|
1279
|
+
NonEmpty: (values) => `Values: ${values.join(", ")}`,
|
|
1280
|
+
Empty: () => "No values",
|
|
1281
|
+
})
|
|
1282
|
+
console.log(listResult) // "Values: 1, 2, 3"
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
### Advanced Pattern Matching
|
|
1286
|
+
|
|
1287
|
+
```typescript
|
|
1288
|
+
import { MatchableUtils } from "functype"
|
|
1289
|
+
|
|
1290
|
+
// Create pattern matchers with guards
|
|
1291
|
+
const isPositive = MatchableUtils.when(
|
|
1292
|
+
(n: number) => n > 0,
|
|
1293
|
+
(n) => `Positive: ${n}`,
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
const isZero = MatchableUtils.when(
|
|
1297
|
+
(n: number) => n === 0,
|
|
1298
|
+
() => "Zero",
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
const isNegative = MatchableUtils.when(
|
|
1302
|
+
(n: number) => n < 0,
|
|
1303
|
+
(n) => `Negative: ${n}`,
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
const defaultCase = MatchableUtils.default((n: number) => `Default: ${n}`)
|
|
1307
|
+
|
|
1308
|
+
// Using pattern matching with multiple conditions
|
|
1309
|
+
function describeNumber(num: number): string {
|
|
1310
|
+
return isPositive(num) ?? isZero(num) ?? isNegative(num) ?? defaultCase(num)
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
console.log(describeNumber(42)) // "Positive: 42"
|
|
1314
|
+
console.log(describeNumber(0)) // "Zero"
|
|
1315
|
+
console.log(describeNumber(-10)) // "Negative: -10"
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
## Common Patterns
|
|
1319
|
+
|
|
1320
|
+
### Chaining Operations
|
|
1321
|
+
|
|
1322
|
+
```typescript
|
|
1323
|
+
import { Option, Either, List } from "functype"
|
|
1324
|
+
|
|
1325
|
+
// Option chaining
|
|
1326
|
+
const userInput = Option(" John Doe ")
|
|
1327
|
+
const processedName = userInput
|
|
1328
|
+
.map((s) => s.trim())
|
|
1329
|
+
.filter((s) => s.length > 0)
|
|
1330
|
+
.map((s) => s.toUpperCase())
|
|
1331
|
+
.getOrElse("ANONYMOUS")
|
|
1332
|
+
console.log(processedName) // "JOHN DOE"
|
|
1333
|
+
|
|
1334
|
+
// Either chaining
|
|
1335
|
+
function validateAge(age: number): Either<string, number> {
|
|
1336
|
+
if (!Number.isInteger(age)) return Either.left("Age must be an integer")
|
|
1337
|
+
if (age < 0) return Either.left("Age cannot be negative")
|
|
1338
|
+
if (age > 120) return Either.left("Age is too high")
|
|
1339
|
+
return Either.right(age)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const processAge = (input: string): Either<string, string> => {
|
|
1343
|
+
return Either.tryCatch(
|
|
1344
|
+
() => parseInt(input, 10),
|
|
1345
|
+
() => "Invalid number format",
|
|
1346
|
+
)
|
|
1347
|
+
.flatMap(validateAge)
|
|
1348
|
+
.map((age) => `Valid age: ${age}`)
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
console.log(processAge("35").getOrElse("Invalid age")) // "Valid age: 35"
|
|
1352
|
+
console.log(processAge("abc").getOrElse("Invalid age")) // "Invalid age"
|
|
1353
|
+
|
|
1354
|
+
// List chaining
|
|
1355
|
+
const numbers = List([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
1356
|
+
const result = numbers
|
|
1357
|
+
.filter((n) => n % 2 === 0) // even numbers
|
|
1358
|
+
.map((n) => n * n) // square them
|
|
1359
|
+
.filter((n) => n > 20) // keep those > 20
|
|
1360
|
+
.take(2) // take first 2
|
|
1361
|
+
.foldLeft(0)((acc, n) => acc + n) // sum them
|
|
1362
|
+
console.log(result) // 36 + 64 = 100
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
### Composition with Pipe
|
|
1366
|
+
|
|
1367
|
+
```typescript
|
|
1368
|
+
import { Option, Either, List, pipe } from "functype"
|
|
1369
|
+
|
|
1370
|
+
// Pipe with Option
|
|
1371
|
+
const result1 = pipe(
|
|
1372
|
+
Option(5),
|
|
1373
|
+
(opt) => opt.map((n) => n * 2),
|
|
1374
|
+
(opt) => opt.filter((n) => n > 5),
|
|
1375
|
+
(opt) => opt.getOrElse(0),
|
|
1376
|
+
)
|
|
1377
|
+
console.log(result1) // 10
|
|
1378
|
+
|
|
1379
|
+
// Pipe with Either
|
|
1380
|
+
const result2 = pipe(
|
|
1381
|
+
Either.right<string, number>(42),
|
|
1382
|
+
(either) => either.map((n) => n.toString()),
|
|
1383
|
+
(either) => either.mapLeft((e) => new Error(e)),
|
|
1384
|
+
(either) => either.getOrElse("error"),
|
|
1385
|
+
)
|
|
1386
|
+
console.log(result2) // "42"
|
|
1387
|
+
|
|
1388
|
+
// Pipe with List and multiple data types
|
|
1389
|
+
const result3 = pipe(
|
|
1390
|
+
List([1, 2, 3, 4]),
|
|
1391
|
+
(list) => list.map((n) => n * 2),
|
|
1392
|
+
(list) => Option(list.head().getOrElse(0)),
|
|
1393
|
+
(opt) => opt.filter((n) => n > 5),
|
|
1394
|
+
(opt) => Either.fromNullable(opt.getOrElse(null), "No valid value"),
|
|
1395
|
+
(either) =>
|
|
1396
|
+
either.fold(
|
|
1397
|
+
(err) => `Error: ${err}`,
|
|
1398
|
+
(val) => `Success: ${val}`,
|
|
1399
|
+
),
|
|
1400
|
+
)
|
|
1401
|
+
console.log(result3) // "Success: 8"
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
## Error Handling
|
|
1405
|
+
|
|
1406
|
+
### Option for Nullable Values
|
|
1407
|
+
|
|
1408
|
+
```typescript
|
|
1409
|
+
import { Option } from "functype"
|
|
1410
|
+
|
|
1411
|
+
// Option instead of null checks
|
|
1412
|
+
function findUserById(id: string): Option<User> {
|
|
1413
|
+
const user = database.findUser(id) // might return null
|
|
1414
|
+
return Option(user)
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Usage
|
|
1418
|
+
const userId = "user123"
|
|
1419
|
+
const user = findUserById(userId)
|
|
1420
|
+
|
|
1421
|
+
// No need for null checks
|
|
1422
|
+
const greet = user.map((u) => `Hello, ${u.name}!`).getOrElse("User not found")
|
|
1423
|
+
|
|
1424
|
+
// Method chaining without worrying about null
|
|
1425
|
+
const userCity = user
|
|
1426
|
+
.flatMap((u) => Option(u.address))
|
|
1427
|
+
.flatMap((a) => Option(a.city))
|
|
1428
|
+
.getOrElse("Unknown location")
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
### Either for Error Handling
|
|
1432
|
+
|
|
1433
|
+
```typescript
|
|
1434
|
+
import { Either } from "functype"
|
|
1435
|
+
|
|
1436
|
+
// Define domain-specific errors
|
|
1437
|
+
class ValidationError extends Error {
|
|
1438
|
+
constructor(message: string) {
|
|
1439
|
+
super(message)
|
|
1440
|
+
this.name = "ValidationError"
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
class NotFoundError extends Error {
|
|
1445
|
+
constructor(message: string) {
|
|
1446
|
+
super(message)
|
|
1447
|
+
this.name = "NotFoundError"
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Functions return Either instead of throwing
|
|
1452
|
+
function validateEmail(email: string): Either<ValidationError, string> {
|
|
1453
|
+
const emailRegex = /^[^@]+@[^@]+\.[^@]+$/
|
|
1454
|
+
return emailRegex.test(email) ? Either.right(email) : Either.left(new ValidationError(`Invalid email: ${email}`))
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function findUserByEmail(email: string): Either<NotFoundError, User> {
|
|
1458
|
+
const user = database.findUserByEmail(email)
|
|
1459
|
+
return user ? Either.right(user) : Either.left(new NotFoundError(`User not found for email: ${email}`))
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Chain operations with proper error handling
|
|
1463
|
+
function processUserEmail(email: string): Either<Error, string> {
|
|
1464
|
+
return validateEmail(email)
|
|
1465
|
+
.flatMap((validEmail) => findUserByEmail(validEmail))
|
|
1466
|
+
.map((user) => `User ${user.name} found with email ${email}`)
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// Usage
|
|
1470
|
+
const result = processUserEmail("user@example.com")
|
|
1471
|
+
result.fold(
|
|
1472
|
+
(error) => console.error(`Error: ${error.message}`),
|
|
1473
|
+
(success) => console.log(success),
|
|
1474
|
+
)
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
### Try for Exception Handling
|
|
1478
|
+
|
|
1479
|
+
```typescript
|
|
1480
|
+
import { Try, Option, Either } from "functype"
|
|
1481
|
+
|
|
1482
|
+
// Safely handle potentially throwing code
|
|
1483
|
+
function parseJSON(json: string): Try<unknown> {
|
|
1484
|
+
return Try(() => JSON.parse(json))
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Parse configuration from JSON file
|
|
1488
|
+
function loadConfig(filePath: string): Try<Config> {
|
|
1489
|
+
return Try(() => {
|
|
1490
|
+
const fileContents = fs.readFileSync(filePath, "utf8")
|
|
1491
|
+
return JSON.parse(fileContents) as Config
|
|
1492
|
+
})
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Usage
|
|
1496
|
+
const config = loadConfig("/path/to/config.json").recover({ host: "localhost", port: 8080 }) // Default if loading fails
|
|
1497
|
+
|
|
1498
|
+
// Convert to other types as needed
|
|
1499
|
+
const configOption: Option<Config> = config.toOption()
|
|
1500
|
+
const configEither: Either<Error, Config> = config.toEither()
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
### FPromise for Async Error Handling
|
|
1504
|
+
|
|
1505
|
+
```typescript
|
|
1506
|
+
import { FPromise } from "functype"
|
|
1507
|
+
|
|
1508
|
+
// API client with proper error handling
|
|
1509
|
+
class ApiClient {
|
|
1510
|
+
private baseUrl: string
|
|
1511
|
+
|
|
1512
|
+
constructor(baseUrl: string) {
|
|
1513
|
+
this.baseUrl = baseUrl
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
fetchData<T>(endpoint: string): FPromise<T, Error> {
|
|
1517
|
+
return FPromise.tryCatchAsync(
|
|
1518
|
+
async () => {
|
|
1519
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`)
|
|
1520
|
+
|
|
1521
|
+
if (!response.ok) {
|
|
1522
|
+
throw new Error(`HTTP error: ${response.status}`)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return response.json() as Promise<T>
|
|
1526
|
+
},
|
|
1527
|
+
(error) => {
|
|
1528
|
+
if (error instanceof Error) {
|
|
1529
|
+
return error
|
|
1530
|
+
}
|
|
1531
|
+
return new Error(String(error))
|
|
1532
|
+
},
|
|
1533
|
+
)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
submitData<T, R>(endpoint: string, data: T): FPromise<R, Error> {
|
|
1537
|
+
return FPromise.tryCatchAsync(
|
|
1538
|
+
async () => {
|
|
1539
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
1540
|
+
method: "POST",
|
|
1541
|
+
headers: { "Content-Type": "application/json" },
|
|
1542
|
+
body: JSON.stringify(data),
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
if (!response.ok) {
|
|
1546
|
+
throw new Error(`HTTP error: ${response.status}`)
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return response.json() as Promise<R>
|
|
1550
|
+
},
|
|
1551
|
+
(error) => {
|
|
1552
|
+
if (error instanceof Error) {
|
|
1553
|
+
return error
|
|
1554
|
+
}
|
|
1555
|
+
return new Error(String(error))
|
|
1556
|
+
},
|
|
1557
|
+
)
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Usage
|
|
1562
|
+
const api = new ApiClient("https://api.example.com")
|
|
1563
|
+
|
|
1564
|
+
api
|
|
1565
|
+
.fetchData<User[]>("/users")
|
|
1566
|
+
.map((users) => users.map((u) => u.name))
|
|
1567
|
+
.fold(
|
|
1568
|
+
(error) => console.error(`Failed to fetch users: ${error.message}`),
|
|
1569
|
+
(names) => console.log(`User names: ${names.join(", ")}`),
|
|
1570
|
+
)
|
|
1571
|
+
|
|
1572
|
+
api
|
|
1573
|
+
.submitData<NewUser, User>("/users", { name: "Alice", email: "alice@example.com" })
|
|
1574
|
+
.map((user) => `Created user with ID: ${user.id}`)
|
|
1575
|
+
.recover("Failed to create user")
|
|
1576
|
+
.then((result) => console.log(result))
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
## Real-World Examples
|
|
1580
|
+
|
|
1581
|
+
### Form Validation
|
|
1582
|
+
|
|
1583
|
+
```typescript
|
|
1584
|
+
import { Either, pipe } from "functype"
|
|
1585
|
+
|
|
1586
|
+
// Define validation types
|
|
1587
|
+
type ValidationError = {
|
|
1588
|
+
field: string
|
|
1589
|
+
message: string
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
type FormData = {
|
|
1593
|
+
username: string
|
|
1594
|
+
email: string
|
|
1595
|
+
password: string
|
|
1596
|
+
age: string
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Validation functions
|
|
1600
|
+
const validateUsername = (username: string): Either<ValidationError, string> => {
|
|
1601
|
+
if (!username) {
|
|
1602
|
+
return Either.left({ field: "username", message: "Username is required" })
|
|
1603
|
+
}
|
|
1604
|
+
if (username.length < 3) {
|
|
1605
|
+
return Either.left({ field: "username", message: "Username must be at least 3 characters" })
|
|
1606
|
+
}
|
|
1607
|
+
return Either.right(username)
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const validateEmail = (email: string): Either<ValidationError, string> => {
|
|
1611
|
+
if (!email) {
|
|
1612
|
+
return Either.left({ field: "email", message: "Email is required" })
|
|
1613
|
+
}
|
|
1614
|
+
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) {
|
|
1615
|
+
return Either.left({ field: "email", message: "Invalid email format" })
|
|
1616
|
+
}
|
|
1617
|
+
return Either.right(email)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const validatePassword = (password: string): Either<ValidationError, string> => {
|
|
1621
|
+
if (!password) {
|
|
1622
|
+
return Either.left({ field: "password", message: "Password is required" })
|
|
1623
|
+
}
|
|
1624
|
+
if (password.length < 8) {
|
|
1625
|
+
return Either.left({ field: "password", message: "Password must be at least 8 characters" })
|
|
1626
|
+
}
|
|
1627
|
+
return Either.right(password)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const validateAge = (age: string): Either<ValidationError, number> => {
|
|
1631
|
+
if (!age) {
|
|
1632
|
+
return Either.left({ field: "age", message: "Age is required" })
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const numAge = parseInt(age, 10)
|
|
1636
|
+
if (isNaN(numAge)) {
|
|
1637
|
+
return Either.left({ field: "age", message: "Age must be a number" })
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (numAge < 18) {
|
|
1641
|
+
return Either.left({ field: "age", message: "You must be at least 18 years old" })
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return Either.right(numAge)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Validate form data
|
|
1648
|
+
type ValidatedForm = {
|
|
1649
|
+
username: string
|
|
1650
|
+
email: string
|
|
1651
|
+
password: string
|
|
1652
|
+
age: number
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function validateForm(form: FormData): Either<ValidationError[], ValidatedForm> {
|
|
1656
|
+
const usernameResult = validateUsername(form.username)
|
|
1657
|
+
const emailResult = validateEmail(form.email)
|
|
1658
|
+
const passwordResult = validatePassword(form.password)
|
|
1659
|
+
const ageResult = validateAge(form.age)
|
|
1660
|
+
|
|
1661
|
+
// Collect all errors
|
|
1662
|
+
const errors: ValidationError[] = []
|
|
1663
|
+
|
|
1664
|
+
if (usernameResult.isLeft()) errors.push(usernameResult.getLeft())
|
|
1665
|
+
if (emailResult.isLeft()) errors.push(emailResult.getLeft())
|
|
1666
|
+
if (passwordResult.isLeft()) errors.push(passwordResult.getLeft())
|
|
1667
|
+
if (ageResult.isLeft()) errors.push(ageResult.getLeft())
|
|
1668
|
+
|
|
1669
|
+
// If there are any errors, return them
|
|
1670
|
+
if (errors.length > 0) {
|
|
1671
|
+
return Either.left(errors)
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Otherwise return the validated form
|
|
1675
|
+
return Either.right({
|
|
1676
|
+
username: usernameResult.get(),
|
|
1677
|
+
email: emailResult.get(),
|
|
1678
|
+
password: passwordResult.get(),
|
|
1679
|
+
age: ageResult.get(),
|
|
1680
|
+
})
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Usage
|
|
1684
|
+
const formData: FormData = {
|
|
1685
|
+
username: "john",
|
|
1686
|
+
email: "john@example.com",
|
|
1687
|
+
password: "password123",
|
|
1688
|
+
age: "25",
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const validationResult = validateForm(formData)
|
|
1692
|
+
|
|
1693
|
+
validationResult.fold(
|
|
1694
|
+
(errors) => {
|
|
1695
|
+
console.log("Validation failed:")
|
|
1696
|
+
errors.forEach((err) => {
|
|
1697
|
+
console.log(`- ${err.field}: ${err.message}`)
|
|
1698
|
+
})
|
|
1699
|
+
},
|
|
1700
|
+
(validData) => {
|
|
1701
|
+
console.log("Form is valid:", validData)
|
|
1702
|
+
// Process the valid form data...
|
|
1703
|
+
},
|
|
1704
|
+
)
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
### Data Fetching and Processing
|
|
1708
|
+
|
|
1709
|
+
```typescript
|
|
1710
|
+
import { FPromise, Option, Either, pipe, List } from "functype"
|
|
1711
|
+
|
|
1712
|
+
// Define types
|
|
1713
|
+
type User = {
|
|
1714
|
+
id: string
|
|
1715
|
+
name: string
|
|
1716
|
+
email: string
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
type Post = {
|
|
1720
|
+
id: string
|
|
1721
|
+
userId: string
|
|
1722
|
+
title: string
|
|
1723
|
+
body: string
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
type Comment = {
|
|
1727
|
+
id: string
|
|
1728
|
+
postId: string
|
|
1729
|
+
name: string
|
|
1730
|
+
email: string
|
|
1731
|
+
body: string
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// API client
|
|
1735
|
+
class ApiClient {
|
|
1736
|
+
private baseUrl: string
|
|
1737
|
+
|
|
1738
|
+
constructor(baseUrl: string) {
|
|
1739
|
+
this.baseUrl = baseUrl
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
fetchUser(userId: string): FPromise<User, Error> {
|
|
1743
|
+
return this.fetchResource<User>(`/users/${userId}`)
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
fetchUserPosts(userId: string): FPromise<Post[], Error> {
|
|
1747
|
+
return this.fetchResource<Post[]>(`/users/${userId}/posts`)
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
fetchPostComments(postId: string): FPromise<Comment[], Error> {
|
|
1751
|
+
return this.fetchResource<Comment[]>(`/posts/${postId}/comments`)
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
private fetchResource<T>(endpoint: string): FPromise<T, Error> {
|
|
1755
|
+
return FPromise.tryCatchAsync(
|
|
1756
|
+
async () => {
|
|
1757
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`)
|
|
1758
|
+
|
|
1759
|
+
if (!response.ok) {
|
|
1760
|
+
throw new Error(`HTTP error: ${response.status}`)
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
return response.json() as Promise<T>
|
|
1764
|
+
},
|
|
1765
|
+
(error) => new Error(`API error: ${error}`),
|
|
1766
|
+
)
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Use case: fetch a user's posts with comments
|
|
1771
|
+
async function getUserContentSummary(userId: string) {
|
|
1772
|
+
const api = new ApiClient("https://jsonplaceholder.typicode.com")
|
|
1773
|
+
|
|
1774
|
+
// Fetch user
|
|
1775
|
+
const userResult = await api.fetchUser(userId)
|
|
1776
|
+
|
|
1777
|
+
if (userResult.isLeft()) {
|
|
1778
|
+
return `Error fetching user: ${userResult.getLeft().message}`
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const user = userResult.get()
|
|
1782
|
+
|
|
1783
|
+
// Fetch user's posts
|
|
1784
|
+
const postsResult = await api.fetchUserPosts(userId)
|
|
1785
|
+
|
|
1786
|
+
if (postsResult.isLeft()) {
|
|
1787
|
+
return `Error fetching posts: ${postsResult.getLeft().message}`
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const posts = postsResult.get()
|
|
1791
|
+
|
|
1792
|
+
if (posts.length === 0) {
|
|
1793
|
+
return `User ${user.name} has no posts.`
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// Take the latest post
|
|
1797
|
+
const latestPost = posts[0]
|
|
1798
|
+
|
|
1799
|
+
// Fetch comments for the latest post
|
|
1800
|
+
const commentsResult = await api.fetchPostComments(latestPost.id)
|
|
1801
|
+
|
|
1802
|
+
if (commentsResult.isLeft()) {
|
|
1803
|
+
return `Error fetching comments: ${commentsResult.getLeft().message}`
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const comments = commentsResult.get()
|
|
1807
|
+
|
|
1808
|
+
// Create summary
|
|
1809
|
+
return {
|
|
1810
|
+
user: {
|
|
1811
|
+
name: user.name,
|
|
1812
|
+
email: user.email,
|
|
1813
|
+
},
|
|
1814
|
+
postsCount: posts.length,
|
|
1815
|
+
latestPost: {
|
|
1816
|
+
title: latestPost.title,
|
|
1817
|
+
body: latestPost.body.substring(0, 100) + "...",
|
|
1818
|
+
commentsCount: comments.length,
|
|
1819
|
+
},
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Usage
|
|
1824
|
+
getUserContentSummary("1")
|
|
1825
|
+
.then((result) => console.log(JSON.stringify(result, null, 2)))
|
|
1826
|
+
.catch((error) => console.error("Error:", error))
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1829
|
+
### Event Sourcing
|
|
1830
|
+
|
|
1831
|
+
```typescript
|
|
1832
|
+
import { Either, Option, List, Map } from "functype"
|
|
1833
|
+
|
|
1834
|
+
// Define domain types
|
|
1835
|
+
type Event = {
|
|
1836
|
+
id: string
|
|
1837
|
+
timestamp: number
|
|
1838
|
+
type: string
|
|
1839
|
+
payload: unknown
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
type UserCreatedEvent = Event & {
|
|
1843
|
+
type: "UserCreated"
|
|
1844
|
+
payload: {
|
|
1845
|
+
id: string
|
|
1846
|
+
name: string
|
|
1847
|
+
email: string
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
type UserUpdatedEvent = Event & {
|
|
1852
|
+
type: "UserUpdated"
|
|
1853
|
+
payload: {
|
|
1854
|
+
id: string
|
|
1855
|
+
name?: string
|
|
1856
|
+
email?: string
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
type ItemAddedToCartEvent = Event & {
|
|
1861
|
+
type: "ItemAddedToCart"
|
|
1862
|
+
payload: {
|
|
1863
|
+
userId: string
|
|
1864
|
+
productId: string
|
|
1865
|
+
quantity: number
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
type ItemRemovedFromCartEvent = Event & {
|
|
1870
|
+
type: "ItemRemovedFromCart"
|
|
1871
|
+
payload: {
|
|
1872
|
+
userId: string
|
|
1873
|
+
productId: string
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
type CheckoutCompletedEvent = Event & {
|
|
1878
|
+
type: "CheckoutCompleted"
|
|
1879
|
+
payload: {
|
|
1880
|
+
userId: string
|
|
1881
|
+
total: number
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Type guard functions
|
|
1886
|
+
const isUserCreated = (event: Event): event is UserCreatedEvent => event.type === "UserCreated"
|
|
1887
|
+
|
|
1888
|
+
const isUserUpdated = (event: Event): event is UserUpdatedEvent => event.type === "UserUpdated"
|
|
1889
|
+
|
|
1890
|
+
const isItemAddedToCart = (event: Event): event is ItemAddedToCartEvent => event.type === "ItemAddedToCart"
|
|
1891
|
+
|
|
1892
|
+
const isItemRemovedFromCart = (event: Event): event is ItemRemovedFromCartEvent => event.type === "ItemRemovedFromCart"
|
|
1893
|
+
|
|
1894
|
+
const isCheckoutCompleted = (event: Event): event is CheckoutCompletedEvent => event.type === "CheckoutCompleted"
|
|
1895
|
+
|
|
1896
|
+
// User state derived from events
|
|
1897
|
+
type User = {
|
|
1898
|
+
id: string
|
|
1899
|
+
name: string
|
|
1900
|
+
email: string
|
|
1901
|
+
cart: Map<string, number> // productId -> quantity
|
|
1902
|
+
checkoutHistory: List<{
|
|
1903
|
+
timestamp: number
|
|
1904
|
+
total: number
|
|
1905
|
+
}>
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// Event store
|
|
1909
|
+
class EventStore {
|
|
1910
|
+
private events: List<Event> = List([])
|
|
1911
|
+
|
|
1912
|
+
append(event: Event): void {
|
|
1913
|
+
this.events = this.events.add(event)
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
appendMany(events: Event[]): void {
|
|
1917
|
+
this.events = this.events.concat(List(events))
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
getAllEvents(): List<Event> {
|
|
1921
|
+
return this.events
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
getEventsByUserId(userId: string): List<Event> {
|
|
1925
|
+
return this.events.filter((event) => {
|
|
1926
|
+
if (isUserCreated(event) || isUserUpdated(event)) {
|
|
1927
|
+
return (event.payload as { id: string }).id === userId
|
|
1928
|
+
}
|
|
1929
|
+
if (isItemAddedToCart(event) || isItemRemovedFromCart(event) || isCheckoutCompleted(event)) {
|
|
1930
|
+
return (event.payload as { userId: string }).userId === userId
|
|
1931
|
+
}
|
|
1932
|
+
return false
|
|
1933
|
+
})
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// User projection
|
|
1938
|
+
class UserProjection {
|
|
1939
|
+
getUserState(userId: string, events: List<Event>): Option<User> {
|
|
1940
|
+
// Filter events for this user
|
|
1941
|
+
const userEvents = events.filter((event) => {
|
|
1942
|
+
if (isUserCreated(event) || isUserUpdated(event)) {
|
|
1943
|
+
return (event.payload as { id: string }).id === userId
|
|
1944
|
+
}
|
|
1945
|
+
if (isItemAddedToCart(event) || isItemRemovedFromCart(event) || isCheckoutCompleted(event)) {
|
|
1946
|
+
return (event.payload as { userId: string }).userId === userId
|
|
1947
|
+
}
|
|
1948
|
+
return false
|
|
1949
|
+
})
|
|
1950
|
+
|
|
1951
|
+
// Find user creation event
|
|
1952
|
+
const creationEvent = userEvents.find(isUserCreated)
|
|
1953
|
+
|
|
1954
|
+
if (creationEvent.isEmpty()) {
|
|
1955
|
+
return Option(null) // User not found
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Initial state from creation event
|
|
1959
|
+
let user: User = {
|
|
1960
|
+
id: creationEvent.get().payload.id,
|
|
1961
|
+
name: creationEvent.get().payload.name,
|
|
1962
|
+
email: creationEvent.get().payload.email,
|
|
1963
|
+
cart: Map<string, number>({}),
|
|
1964
|
+
checkoutHistory: List([]),
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// Apply all other events in order
|
|
1968
|
+
const remainingEvents = userEvents.filter((e) => e.id !== creationEvent.get().id)
|
|
1969
|
+
|
|
1970
|
+
return Option(
|
|
1971
|
+
remainingEvents.foldLeft(user)((state, event) => {
|
|
1972
|
+
if (isUserUpdated(event)) {
|
|
1973
|
+
return {
|
|
1974
|
+
...state,
|
|
1975
|
+
name: event.payload.name ?? state.name,
|
|
1976
|
+
email: event.payload.email ?? state.email,
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
if (isItemAddedToCart(event)) {
|
|
1981
|
+
return {
|
|
1982
|
+
...state,
|
|
1983
|
+
cart: state.cart.add(event.payload.productId, event.payload.quantity),
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
if (isItemRemovedFromCart(event)) {
|
|
1988
|
+
return {
|
|
1989
|
+
...state,
|
|
1990
|
+
cart: state.cart.remove(event.payload.productId),
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (isCheckoutCompleted(event)) {
|
|
1995
|
+
return {
|
|
1996
|
+
...state,
|
|
1997
|
+
cart: Map<string, number>({}), // Clear cart
|
|
1998
|
+
checkoutHistory: state.checkoutHistory.add({
|
|
1999
|
+
timestamp: event.timestamp,
|
|
2000
|
+
total: event.payload.total,
|
|
2001
|
+
}),
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
return state
|
|
2006
|
+
}),
|
|
2007
|
+
)
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Usage example
|
|
2012
|
+
const eventStore = new EventStore()
|
|
2013
|
+
const userProjection = new UserProjection()
|
|
2014
|
+
|
|
2015
|
+
// Create some events
|
|
2016
|
+
const events: Event[] = [
|
|
2017
|
+
{
|
|
2018
|
+
id: "e1",
|
|
2019
|
+
timestamp: Date.now() - 5000,
|
|
2020
|
+
type: "UserCreated",
|
|
2021
|
+
payload: {
|
|
2022
|
+
id: "user1",
|
|
2023
|
+
name: "John Doe",
|
|
2024
|
+
email: "john@example.com",
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
id: "e2",
|
|
2029
|
+
timestamp: Date.now() - 4000,
|
|
2030
|
+
type: "ItemAddedToCart",
|
|
2031
|
+
payload: {
|
|
2032
|
+
userId: "user1",
|
|
2033
|
+
productId: "product1",
|
|
2034
|
+
quantity: 2,
|
|
2035
|
+
},
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
id: "e3",
|
|
2039
|
+
timestamp: Date.now() - 3500,
|
|
2040
|
+
type: "ItemAddedToCart",
|
|
2041
|
+
payload: {
|
|
2042
|
+
userId: "user1",
|
|
2043
|
+
productId: "product2",
|
|
2044
|
+
quantity: 1,
|
|
2045
|
+
},
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
id: "e4",
|
|
2049
|
+
timestamp: Date.now() - 3000,
|
|
2050
|
+
type: "ItemRemovedFromCart",
|
|
2051
|
+
payload: {
|
|
2052
|
+
userId: "user1",
|
|
2053
|
+
productId: "product1",
|
|
2054
|
+
},
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
id: "e5",
|
|
2058
|
+
timestamp: Date.now() - 2000,
|
|
2059
|
+
type: "CheckoutCompleted",
|
|
2060
|
+
payload: {
|
|
2061
|
+
userId: "user1",
|
|
2062
|
+
total: 29.99,
|
|
2063
|
+
},
|
|
2064
|
+
},
|
|
2065
|
+
{
|
|
2066
|
+
id: "e6",
|
|
2067
|
+
timestamp: Date.now() - 1000,
|
|
2068
|
+
type: "UserUpdated",
|
|
2069
|
+
payload: {
|
|
2070
|
+
id: "user1",
|
|
2071
|
+
email: "john.doe@example.com",
|
|
2072
|
+
},
|
|
2073
|
+
},
|
|
2074
|
+
]
|
|
2075
|
+
|
|
2076
|
+
// Add events to store
|
|
2077
|
+
eventStore.appendMany(events)
|
|
2078
|
+
|
|
2079
|
+
// Get user state
|
|
2080
|
+
const userState = userProjection.getUserState("user1", eventStore.getAllEvents())
|
|
2081
|
+
|
|
2082
|
+
userState.fold(
|
|
2083
|
+
() => console.log("User not found"),
|
|
2084
|
+
(user) => {
|
|
2085
|
+
console.log("User:", user.name, user.email)
|
|
2086
|
+
console.log("Cart items:", user.cart.size())
|
|
2087
|
+
console.log("Checkout history:", user.checkoutHistory.size(), "orders")
|
|
2088
|
+
console.log("Latest order total:", user.checkoutHistory.head().getOrElse({ total: 0 }).total)
|
|
2089
|
+
},
|
|
2090
|
+
)
|
|
2091
|
+
```
|
|
2092
|
+
|
|
2093
|
+
These examples cover a wide range of use cases and demonstrate how to use Functype effectively in real-world applications. Feel free to adapt them to your specific needs!
|