functype 0.60.2 → 0.60.4

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