@railway-ts/pipelines 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +811 -0
  2. package/dist/composition/index.cjs +72 -0
  3. package/dist/composition/index.cjs.map +1 -0
  4. package/dist/composition/index.d.cts +286 -0
  5. package/dist/composition/index.d.ts +286 -0
  6. package/dist/composition/index.mjs +65 -0
  7. package/dist/composition/index.mjs.map +1 -0
  8. package/dist/index-BdfKTZ7O.d.cts +799 -0
  9. package/dist/index-BdfKTZ7O.d.ts +799 -0
  10. package/dist/index.cjs +1074 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +3 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.mjs +969 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/option/index.cjs +111 -0
  17. package/dist/option/index.cjs.map +1 -0
  18. package/dist/option/index.d.cts +1 -0
  19. package/dist/option/index.d.ts +1 -0
  20. package/dist/option/index.mjs +93 -0
  21. package/dist/option/index.mjs.map +1 -0
  22. package/dist/result/index.cjs +178 -0
  23. package/dist/result/index.cjs.map +1 -0
  24. package/dist/result/index.d.cts +1 -0
  25. package/dist/result/index.d.ts +1 -0
  26. package/dist/result/index.mjs +152 -0
  27. package/dist/result/index.mjs.map +1 -0
  28. package/dist/schema/index.cjs +794 -0
  29. package/dist/schema/index.cjs.map +1 -0
  30. package/dist/schema/index.d.cts +1867 -0
  31. package/dist/schema/index.d.ts +1867 -0
  32. package/dist/schema/index.mjs +735 -0
  33. package/dist/schema/index.mjs.map +1 -0
  34. package/examples/complete-pipelines/async-launch.ts +128 -0
  35. package/examples/complete-pipelines/async.ts +119 -0
  36. package/examples/complete-pipelines/hill-clohessy-wiltshire.ts +218 -0
  37. package/examples/complete-pipelines/hohmann-transfer.ts +159 -0
  38. package/examples/composition/advanced-composition.ts +32 -0
  39. package/examples/composition/curry-basics.ts +24 -0
  40. package/examples/composition/tupled-basics.ts +26 -0
  41. package/examples/index.ts +47 -0
  42. package/examples/interop/interop-examples.ts +110 -0
  43. package/examples/option/option-examples.ts +63 -0
  44. package/examples/result/result-examples.ts +110 -0
  45. package/examples/schema/basic.ts +78 -0
  46. package/examples/schema/union.ts +301 -0
  47. package/package.json +100 -0
package/README.md ADDED
@@ -0,0 +1,811 @@
1
+ # @railway-ts/pipelines
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@railway-ts/pipelines.svg)](https://www.npmjs.com/package/@railway-ts/pipelines) [![Build Status](https://github.com/sakobu/railway-ts-pipelines/workflows/CI/badge.svg)](https://github.com/sakobu/railway-ts-pipelines/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@railway-ts/pipelines)](https://bundlephobia.com/package/@railway-ts/pipelines) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)](https://www.typescriptlang.org/) [![Coverage](https://img.shields.io/codecov/c/github/sakobu/railway-ts-pipelines)](https://codecov.io/gh/sakobu/railway-ts-pipelines)
4
+
5
+ **Make failure boring. Make data flow.**
6
+
7
+ A type-safe toolkit for TypeScript implementing railway-oriented programming. Build robust data pipelines with zero classes, zero exceptions, and zero `any`. Model uncertainty with `Option` and `Result`, validate once at boundaries, and let errors flow naturally through your code.
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ ### Getting Started
14
+
15
+ - [The Problem: Error Handling is Messy](#the-problem-error-handling-is-messy)
16
+ - [The Solution: Railway-Oriented Programming](#the-solution-railway-oriented-programming)
17
+ - [Installation](#installation)
18
+ - [Running the Examples](#running-the-examples)
19
+
20
+ ### Core Concepts
21
+
22
+ - [The Building Blocks](#the-building-blocks)
23
+ - [Option: Handle Absence as Data](#option-handle-absence-as-data)
24
+ - [Result: Railway-Oriented Error Handling](#result-railway-oriented-error-handling)
25
+ - [Schema: Parse, Don't Validate](#schema-parse-dont-validate)
26
+ - [Composition: Build Complex Pipelines](#composition-build-complex-pipelines)
27
+
28
+ ### Practical Guides
29
+
30
+ - [Import Strategy](#import-strategy)
31
+ - [Quick Reference](#quick-reference)
32
+
33
+ ### Decision Guides
34
+
35
+ - [Is This Right For You?](#is-this-right-for-you)
36
+
37
+ ### Reference
38
+
39
+ - [Next Steps](#next-steps)
40
+ - [Further Reading](#further-reading)
41
+
42
+ ---
43
+
44
+ ## The Problem: Error Handling is Messy
45
+
46
+ Most TypeScript codebases struggle with error handling. Sound familiar?
47
+
48
+ ```typescript
49
+ // Traditional approach: nested try/catch, type assertions, repeated checks
50
+ function processOrder(raw: unknown) {
51
+ try {
52
+ if (!raw || typeof raw !== 'object') throw new Error('Invalid input');
53
+ const amount = (raw as any).amount;
54
+ if (typeof amount !== 'number' || amount <= 0) throw new Error('Invalid amount');
55
+ const result = await chargeCard(amount);
56
+ if (!result.success) throw new Error(result.error);
57
+ return { success: true, data: result };
58
+ } catch (e) {
59
+ return { success: false, error: String(e) };
60
+ }
61
+ }
62
+ ```
63
+
64
+ **Problems:**
65
+
66
+ - Exception-based control flow escapes your types
67
+ - Type assertions (`as any`) break type safety
68
+ - Repeated validation adds ceremony
69
+ - Nested conditionals obscure the happy path
70
+
71
+ **What if there was a better way?**
72
+
73
+ ```typescript
74
+ // Railway-oriented: validate once, compose pure functions, branch at edges
75
+ const orderSchema = object({
76
+ amount: required(chain(number(), min(0.01))),
77
+ });
78
+
79
+ const processOrder = flow(
80
+ (input: unknown) => validate(input, orderSchema),
81
+ (r) => andThen(r, ({ amount }) => chargeCard(amount)),
82
+ );
83
+ // Errors automatically stay on the error track. No nested conditionals.
84
+ ```
85
+
86
+ ---
87
+
88
+ ## The Solution: Railway-Oriented Programming
89
+
90
+ **Railway-oriented programming** treats your code like a railway track with two parallel rails:
91
+
92
+ ```
93
+ Input --> validate --> transform --> compute --> Output
94
+ | |
95
+ | (validation error) |
96
+ +-------> Error Track ----------------+
97
+ (auto-propagates)
98
+ ```
99
+
100
+ ### How It Works
101
+
102
+ 1. **Valid data** stays on the success track
103
+ 2. **Errors** automatically switch to the error track
104
+ 3. **No manual checking** - transformations skip when already on error track
105
+ 4. **Branch once** at the very end with pattern matching
106
+
107
+ ### Core Philosophy
108
+
109
+ This library brings this pattern to TypeScript with three principles:
110
+
111
+ 1. **Errors and Absence are Values** - Not exceptions. Use `Result<T, E>` for operations that can fail, `Option<T>` for optional values
112
+ 2. **Parse, Don't Validate** - Don't just check data; transform it into guaranteed-valid types
113
+ 3. **Compose Everything** - Build complex workflows by piping simple functions
114
+
115
+ ### The Pipeline Mental Model
116
+
117
+ ```
118
+ unknown input -> validate -> Result<T, E> -> transform -> compute -> Result<U, E>
119
+ | | |
120
+ boundary type-safe stays on rails
121
+ ```
122
+
123
+ - **Boundary**: Untrusted data enters as `unknown`
124
+ - **Validate**: Convert to `Result<T, E>` using schema validators
125
+ - **Type-Safe Core**: Transform validated data with pure functions
126
+ - **Stay On Rails**: Errors propagate automatically through `map`/`flatMap`
127
+ - **Branch Once**: Use `match()` at the edges to handle both paths
128
+
129
+ ---
130
+
131
+ ## Installation
132
+
133
+ ```bash
134
+ bun add @railway-ts/pipelines
135
+ # or npm install @railway-ts/pipelines
136
+ ```
137
+
138
+ **New to the library?** Check out the **[Getting Started Guide](GETTING_STARTED.md)** for a step-by-step tutorial with complete examples.
139
+
140
+ ---
141
+
142
+ ## Running the Examples
143
+
144
+ The library includes 12+ runnable examples covering Option, Result, Schema validation, Composition patterns, and complete real-world pipelines.
145
+
146
+ ```bash
147
+ # Clone and explore
148
+ git clone https://github.com/sakobu/railway-ts-pipelines.git
149
+ cd railway-ts-pipelines
150
+ bun install
151
+
152
+ # Run all examples
153
+ bun run examples/index.ts
154
+
155
+ # Or run specific categories
156
+ bun run examples/complete-pipelines/async-launch.ts # Rocket launch decision system
157
+ ```
158
+
159
+ **See the full guide**: **[Running the Examples ->](GETTING_STARTED.md#running-the-examples)** with all commands and what each demonstrates.
160
+
161
+ ---
162
+
163
+ ## The Building Blocks
164
+
165
+ ### Option: Handle Absence as Data
166
+
167
+ Replace `null`, `undefined`, and nullable types with explicit `Option<T>`.
168
+
169
+ #### Core Type
170
+
171
+ ```typescript
172
+ type Option<T> = { readonly some: true; readonly value: T } | { readonly some: false };
173
+ ```
174
+
175
+ #### When to Use
176
+
177
+ Use `Option` when **absence is expected and normal**: finding items in collections, optional configuration, nullable database fields. Don't use it when you need to carry error information (use `Result` instead).
178
+
179
+ #### Basic Usage
180
+
181
+ ```typescript
182
+ import { some, none, fromNullable, map, unwrapOr, match } from '@railway-ts/pipelines/option';
183
+
184
+ // Create Options
185
+ const hasValue = some(42);
186
+ const noValue = none<number>();
187
+
188
+ // Convert from nullable
189
+ const user: { email?: string } = getUser();
190
+ const email = pipe(
191
+ fromNullable(user.email),
192
+ (opt) => map(opt, (e) => e.toLowerCase()),
193
+ (opt) => unwrapOr(opt, 'no-email@example.com'),
194
+ );
195
+
196
+ // Pattern matching
197
+ match(fromNullable(user.email), {
198
+ some: (email) => sendWelcome(email),
199
+ none: () => console.log('No email provided'),
200
+ });
201
+ ```
202
+
203
+ #### API Overview
204
+
205
+ | Category | Functions |
206
+ | ------------- | ------------------------------------------------------------------------- |
207
+ | **Create** | `some(value)`, `none()` |
208
+ | **Check** | `isSome(option)`, `isNone(option)` |
209
+ | **Transform** | `map(option, fn)`, `flatMap(option, fn)`, `filter(option, pred)` |
210
+ | **Extract** | `unwrap(option)`, `unwrapOr(option, default)`, `unwrapOrElse(option, fn)` |
211
+ | **Combine** | `combine(options)` - tuple-preserving, returns `none` if any is `none` |
212
+ | **Convert** | `fromNullable(value)`, `mapToResult(option, error)` |
213
+ | **Match** | `match(option, { some, none })` |
214
+
215
+ **See full documentation**: [`src/option/option.ts`](src/option/option.ts) (21 functions)
216
+ **See examples**: [`examples/option/option-examples.ts`](examples/option/option-examples.ts)
217
+
218
+ ---
219
+
220
+ ### Result: Railway-Oriented Error Handling
221
+
222
+ Model success and failure explicitly. Compose operations without exceptions.
223
+
224
+ #### Core Type
225
+
226
+ ```typescript
227
+ type Result<T, E> = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: E };
228
+ ```
229
+
230
+ #### When to Use
231
+
232
+ Use `Result` when **operations can fail and you need error context**: parsing data, I/O operations, business logic validations. You need to explain _why_ something failed, not just that it did.
233
+
234
+ #### Basic Usage
235
+
236
+ ```typescript
237
+ import { ok, err, map, flatMap, match, fromTry } from '@railway-ts/pipelines/result';
238
+ import { pipe } from '@railway-ts/pipelines/composition';
239
+
240
+ // Operations that can fail return Result
241
+ const safeDivide = (a: number, b: number): Result<number, string> => (b === 0 ? err('Division by zero') : ok(a / b));
242
+
243
+ // Chain operations on the happy path
244
+ const process = (input: string) =>
245
+ pipe(
246
+ fromTry(() => JSON.parse(input)),
247
+ (r) => flatMap(r, (data) => safeDivide(data.value, 2)),
248
+ (r) => map(r, Math.round),
249
+ );
250
+
251
+ // Branch once at the end
252
+ match(process('{"value":42}'), {
253
+ ok: (n) => console.log('Result', n),
254
+ err: (e) => console.error('Failed', e),
255
+ });
256
+ ```
257
+
258
+ #### API Overview
259
+
260
+ | Category | Functions |
261
+ | ---------------- | ------------------------------------------------------------------------------------------------- |
262
+ | **Create** | `ok(value)`, `err(error)` |
263
+ | **Check** | `isOk(result)`, `isErr(result)` |
264
+ | **Transform** | `map(result, fn)`, `mapErr(result, fn)`, `flatMap(result, fn)` |
265
+ | **Extract** | `unwrap(result)`, `unwrapOr(result, default)`, `unwrapOrElse(result, fn)` |
266
+ | **Side Effects** | `tap(result, fn)`, `tapErr(result, fn)` |
267
+ | **Combine** | `combine(results)` - short-circuits on first error<br>`combineAll(results)` - collects all errors |
268
+ | **Async** | `fromPromise(promise)`, `toPromise(result)`, `andThen(result, asyncFn)` |
269
+ | **Convert** | `fromTry(fn)`, `mapToOption(result)` |
270
+ | **Match** | `match(result, { ok, err })` |
271
+
272
+ **Critical for async pipelines**: Use `andThen(result, asyncFn)` to chain async operations while keeping them on the railway.
273
+
274
+ **See full documentation**: [`src/result/result.ts`](src/result/result.ts) (27 functions)
275
+ **See examples**: [`examples/result/result-examples.ts`](examples/result/result-examples.ts)
276
+
277
+ ---
278
+
279
+ ### Schema: Parse, Don't Validate
280
+
281
+ Transform untrusted data into guaranteed-valid types. Validate once at boundaries, then work with confidence.
282
+
283
+ #### Philosophy: Validation as Transformation
284
+
285
+ Traditional validation checks data but leaves it as `unknown` or `any`. Railway-ts validators **parse and transform**:
286
+
287
+ ```typescript
288
+ // BAD: Traditional validation without transformation
289
+ function process(input: any) {
290
+ if (!input.age || typeof input.age !== 'number') throw new Error();
291
+ if (input.age < 18) throw new Error();
292
+ return doSomething(input.age); // input is still 'any'
293
+ }
294
+
295
+ // GOOD: Railway-ts parse into guaranteed-valid types
296
+ const ageValidator = chain(parseNumber(), min(18));
297
+ // Validator<unknown, number> - transforms unknown -> number
298
+
299
+ const result = validate(input.age, ageValidator);
300
+ // result: Result<number, ValidationError[]>
301
+ // If ok, TypeScript knows it's a number >= 18
302
+ ```
303
+
304
+ #### Core Types
305
+
306
+ ```typescript
307
+ type Validator<Input, Output = Input> = (value: Input, path?: string[]) => Result<Output, ValidationError[]>;
308
+
309
+ type ValidationError = {
310
+ path: string[]; // e.g., ['user', 'address', 'zipCode']
311
+ message: string;
312
+ };
313
+
314
+ // Extract output type from validator
315
+ type InferSchemaType<V> = V extends Validator<unknown, infer O> ? ProcessType<O> : never;
316
+ ```
317
+
318
+ #### Basic Validators
319
+
320
+ ```typescript
321
+ import {
322
+ validate,
323
+ string,
324
+ number,
325
+ boolean,
326
+ date,
327
+ object,
328
+ required,
329
+ optional,
330
+ nullable,
331
+ chain,
332
+ type InferSchemaType,
333
+ } from '@railway-ts/pipelines/schema';
334
+
335
+ // Primitive validators
336
+ const name = string(); // unknown -> Result<string, Error[]>
337
+ const age = number(); // unknown -> Result<number, Error[]>
338
+ const active = boolean(); // unknown -> Result<boolean, Error[]>
339
+
340
+ // Object schema
341
+ const userSchema = object({
342
+ name: required(string()), // must exist
343
+ email: optional(string()), // may be undefined
344
+ age: nullable(number()), // may be null
345
+ });
346
+
347
+ type User = InferSchemaType<typeof userSchema>;
348
+ // { name: string; email?: string; age: number | null }
349
+ ```
350
+
351
+ #### Chaining Validators
352
+
353
+ Use `chain()` to build validation pipelines:
354
+
355
+ ```typescript
356
+ import { chain, parseNumber, min, max, integer } from '@railway-ts/pipelines/schema';
357
+
358
+ // Sequential validation + transformation
359
+ const adultAge = chain(
360
+ parseNumber(), // unknown -> number (or error)
361
+ integer(), // number -> number (or error if not integer)
362
+ min(18), // number -> number (or error if < 18)
363
+ max(120), // number -> number (or error if > 120)
364
+ );
365
+
366
+ validate('25', adultAge); // ok(25) - parsed and validated
367
+ validate('15', adultAge); // err([{ path: [], message: "Must be at least 18" }])
368
+ ```
369
+
370
+ #### String, Number, Array Validators
371
+
372
+ ```typescript
373
+ import {
374
+ string,
375
+ minLength,
376
+ maxLength,
377
+ pattern,
378
+ nonEmpty,
379
+ email,
380
+ number,
381
+ min,
382
+ max,
383
+ between,
384
+ integer,
385
+ positive,
386
+ array,
387
+ minItems,
388
+ maxItems,
389
+ unique,
390
+ stringEnum,
391
+ } from '@railway-ts/pipelines/schema';
392
+
393
+ // String validation
394
+ const username = chain(
395
+ string(),
396
+ nonEmpty('Username required'),
397
+ minLength(3, 'Too short'),
398
+ pattern(/^[a-zA-Z0-9_]+$/, 'Alphanumeric only'),
399
+ );
400
+
401
+ // Number validation
402
+ const price = chain(number(), positive('Price must be positive'), precision(2, 'Max 2 decimal places'));
403
+
404
+ // Array validation
405
+ const tags = chain(
406
+ array(string()),
407
+ minItems(1, 'At least one tag required'),
408
+ maxItems(10, 'Max 10 tags'),
409
+ unique('Duplicate tags not allowed'),
410
+ );
411
+
412
+ // Enum validation
413
+ const status = stringEnum(['pending', 'approved', 'rejected'] as const);
414
+ // Returns: Validator<unknown, 'pending' | 'approved' | 'rejected'>
415
+ ```
416
+
417
+ #### Parsing Validators (Type Transformations)
418
+
419
+ These validators transform types during validation:
420
+
421
+ ```typescript
422
+ import {
423
+ parseNumber, // string -> number
424
+ parseDate, // string -> Date
425
+ parseJSON, // string -> unknown
426
+ parseEnum, // string -> EnumValue
427
+ } from '@railway-ts/pipelines/schema';
428
+
429
+ // Convert string to number in validation pipeline
430
+ const ageFromString = chain(parseNumber('Invalid number'), min(18, 'Must be adult'));
431
+
432
+ validate('25', ageFromString); // ok(25) - note: number, not string
433
+
434
+ // Parse JSON and validate structure
435
+ const jsonUserSchema = chain(
436
+ parseJSON(),
437
+ object({
438
+ name: required(string()),
439
+ age: required(number()),
440
+ }),
441
+ );
442
+
443
+ validate('{"name":"Alice","age":30}', jsonUserSchema);
444
+ // ok({ name: "Alice", age: 30 })
445
+ ```
446
+
447
+ #### Union Types
448
+
449
+ ```typescript
450
+ import { union, discriminatedUnion, literal } from '@railway-ts/pipelines/schema';
451
+
452
+ // Try validators in order (first success wins)
453
+ const stringOrNumber = union([string(), parseNumber()]);
454
+
455
+ // Discriminated unions (more efficient)
456
+ const shapeSchema = discriminatedUnion('type', {
457
+ circle: object({
458
+ type: required(literal('circle')),
459
+ radius: required(number()),
460
+ }),
461
+ rectangle: object({
462
+ type: required(literal('rectangle')),
463
+ width: required(number()),
464
+ height: required(number()),
465
+ }),
466
+ });
467
+
468
+ type Shape = InferSchemaType<typeof shapeSchema>;
469
+ // { type: 'circle', radius: number } | { type: 'rectangle', width: number, height: number }
470
+ ```
471
+
472
+ #### Complete Schema Example
473
+
474
+ ```typescript
475
+ import {
476
+ validate,
477
+ object,
478
+ required,
479
+ optional,
480
+ chain,
481
+ string,
482
+ minLength,
483
+ maxLength,
484
+ email,
485
+ parseNumber,
486
+ integer,
487
+ min,
488
+ max,
489
+ array,
490
+ minItems,
491
+ maxItems,
492
+ stringEnum,
493
+ formatErrors,
494
+ type InferSchemaType,
495
+ } from '@railway-ts/pipelines/schema';
496
+
497
+ const createUserSchema = object({
498
+ username: required(chain(string(), minLength(3), maxLength(20))),
499
+ email: required(chain(string(), email())),
500
+ bio: optional(string()),
501
+ age: required(chain(parseNumber(), integer(), min(13), max(120))),
502
+ interests: required(chain(array(string()), minItems(1), maxItems(10))),
503
+ role: required(stringEnum(['user', 'admin', 'moderator'] as const)),
504
+ });
505
+
506
+ type CreateUserInput = InferSchemaType<typeof createUserSchema>;
507
+ // {
508
+ // username: string;
509
+ // email: string;
510
+ // bio?: string;
511
+ // age: number;
512
+ // interests: string[];
513
+ // role: 'user' | 'admin' | 'moderator';
514
+ // }
515
+
516
+ function createUser(input: unknown): Result<User, Record<string, string>> {
517
+ const validated = validate(input, createUserSchema);
518
+
519
+ return match(validated, {
520
+ ok: (data) => saveToDatabase(data), // data is CreateUserInput
521
+ err: (errors) => err(formatErrors(errors)),
522
+ });
523
+ }
524
+ ```
525
+
526
+ **See full validator catalog**: [`src/schema/`](src/schema/) (50+ validators)
527
+ **See examples**: [`examples/schema/`](examples/schema/)
528
+
529
+ ---
530
+
531
+ ### Composition: Build Complex Pipelines
532
+
533
+ Functional composition utilities for building data pipelines.
534
+
535
+ #### pipe() - Immediate Composition
536
+
537
+ Left-to-right data flow with immediate execution:
538
+
539
+ ```typescript
540
+ import { pipe } from '@railway-ts/pipelines/composition';
541
+
542
+ const result = pipe(
543
+ 5,
544
+ (x) => x * 2, // 10
545
+ (x) => x + 1, // 11
546
+ (x) => String(x), // "11"
547
+ );
548
+ // "11"
549
+ ```
550
+
551
+ Supports up to 10 steps with full type inference at each stage.
552
+
553
+ #### flow() - Deferred Composition
554
+
555
+ Build reusable function pipelines:
556
+
557
+ ```typescript
558
+ import { flow } from '@railway-ts/pipelines/composition';
559
+
560
+ const processNumber = flow(
561
+ (x: number) => x * 2,
562
+ (x) => x + 1,
563
+ (x) => String(x),
564
+ );
565
+
566
+ processNumber(5); // "11"
567
+ processNumber(10); // "21"
568
+ ```
569
+
570
+ First function can accept multiple arguments:
571
+
572
+ ```typescript
573
+ const processSum = flow(
574
+ (a: number, b: number) => a + b,
575
+ (x) => x * 2,
576
+ String,
577
+ );
578
+
579
+ processSum(3, 4); // "14"
580
+ ```
581
+
582
+ #### curry() / tupled()
583
+
584
+ Convert between different function forms:
585
+
586
+ ```typescript
587
+ import { curry, tupled } from '@railway-ts/pipelines/composition';
588
+
589
+ const add = (a: number, b: number) => a + b;
590
+
591
+ // Currying for partial application
592
+ const curriedAdd = curry(add);
593
+ curriedAdd(5)(3); // 8
594
+
595
+ pipe(5, curry(add)(10)); // 15
596
+
597
+ // Tupled for array destructuring
598
+ const tupledAdd = tupled(add);
599
+ tupledAdd([5, 3]); // 8
600
+ ```
601
+
602
+ **See full API**: [`src/composition/`](src/composition/)
603
+ **See examples**: [`examples/composition/`](examples/composition/)
604
+
605
+ ---
606
+
607
+ ## Quick Reference
608
+
609
+ ### When to Use What
610
+
611
+ | Scenario | Use | Why |
612
+ | --------------------------- | ----------------- | ------------------------------------- |
613
+ | Value might be absent | `Option<T>` | Absence is normal, not exceptional |
614
+ | Operation can fail | `Result<T, E>` | Need to communicate why it failed |
615
+ | Validating untrusted input | Schema validators | Parse into guaranteed-valid types |
616
+ | Building data flows | `pipe`, `flow` | Compose transformations left-to-right |
617
+ | Multi-step async operations | `andThen` | Chain async Results without nesting |
618
+
619
+ ### Common Patterns
620
+
621
+ **Boundary Validation**
622
+
623
+ ```typescript
624
+ // unknown -> Result<T, E>
625
+ const result = validate(untrustedInput, schema);
626
+ ```
627
+
628
+ **Transform on Success Path**
629
+
630
+ ```typescript
631
+ // Stay on the rails
632
+ const transformed = andThen(result, (data) => processData(data));
633
+ ```
634
+
635
+ **Branch at the End**
636
+
637
+ ```typescript
638
+ // Handle both paths once
639
+ match(result, {
640
+ ok: (data) => handleSuccess(data),
641
+ err: (errors) => handleErrors(errors),
642
+ });
643
+ ```
644
+
645
+ **Compose Reusable Pipelines**
646
+
647
+ ```typescript
648
+ // Build function pipelines
649
+ const process = flow(
650
+ (input) => validate(input, schema),
651
+ (r) => andThen(r, fetchData),
652
+ (r) => andThen(r, transform),
653
+ );
654
+ ```
655
+
656
+ **Combine Multiple Values**
657
+
658
+ ```typescript
659
+ // All-or-nothing with Option
660
+ const combined = combineOption([some(1), some(2), some(3)]); // Some([1,2,3])
661
+
662
+ // Fail-fast with Result
663
+ const results = combineResult([ok(1), ok(2), ok(3)]); // Ok([1,2,3])
664
+ ```
665
+
666
+ ---
667
+
668
+ ## Import Strategy
669
+
670
+ The library provides **tree-shakable subpath imports**:
671
+
672
+ ### Recommended: Subpath Imports
673
+
674
+ ```typescript
675
+ import { some, none, map } from '@railway-ts/pipelines/option';
676
+ import { ok, err, flatMap } from '@railway-ts/pipelines/result';
677
+ import { pipe, flow } from '@railway-ts/pipelines/composition';
678
+ import { string, number, validate } from '@railway-ts/pipelines/schema';
679
+ ```
680
+
681
+ **Benefits:**
682
+
683
+ - Optimal tree-shaking
684
+ - Natural function names (both Option and Result have `map`, no conflicts)
685
+ - Import only what you need
686
+
687
+ ### Alternative: Root Import
688
+
689
+ ```typescript
690
+ import {
691
+ mapOption, // Option's map
692
+ mapResult, // Result's map
693
+ pipe,
694
+ ok,
695
+ validate,
696
+ } from '@railway-ts/pipelines';
697
+ ```
698
+
699
+ **When to use:** Convenience when you need functions from multiple modules and don't mind renamed functions.
700
+
701
+ **Module structure:**
702
+
703
+ | Import Path | Module | Key Types |
704
+ | ----------------------------------- | -------------------- | ----------------- |
705
+ | `@railway-ts/pipelines/option` | Optional values | `Option<T>` |
706
+ | `@railway-ts/pipelines/result` | Fallible operations | `Result<T, E>` |
707
+ | `@railway-ts/pipelines/schema` | Validation | `Validator<I, O>` |
708
+ | `@railway-ts/pipelines/composition` | Function composition | `pipe`, `flow` |
709
+
710
+ ---
711
+
712
+ ## Is This Right For You?
713
+
714
+ ### When to Use railway-ts/pipelines
715
+
716
+ **Choose railway-ts/pipelines when:**
717
+
718
+ - Building data transformation pipelines
719
+ - You want validation + error handling + composition in one integrated system
720
+ - You prefer functional composition over method chaining
721
+ - You need explicit error propagation through multi-step workflows
722
+ - You want a gentler FP learning curve than fp-ts or Effect-TS
723
+
724
+ ### When to Choose Something Else
725
+
726
+ **Choose an alternative when:**
727
+
728
+ - You only need validation (Zod is simpler for just validation)
729
+ - You need a full effect system with DI and resource management (Effect-TS)
730
+ - You want comprehensive category theory abstractions (fp-ts)
731
+ - You prefer OOP or method chaining style
732
+
733
+ ### Quick Comparison
734
+
735
+ | Library | Scope | Philosophy | Best For |
736
+ | -------------- | ---------------------------- | ------------------------------- | ---------------------------- |
737
+ | **railway-ts** | Railway pattern + validation | Railway-oriented programming | Data pipelines, pragmatic FP |
738
+ | **Zod** | Validation only | Declarative schemas | Drop-in validation |
739
+ | **fp-ts** | Complete FP toolkit | Category theory | FP purists, complex apps |
740
+ | **Effect-TS** | Full effect system | Effect/Fiber/Layer architecture | Large apps, microservices |
741
+
742
+ ---
743
+
744
+ ## Next Steps
745
+
746
+ ### API Reference
747
+
748
+ Full API documentation with types and examples:
749
+
750
+ - **Option**: [`src/option/option.ts`](src/option/option.ts) - 21 functions
751
+ - **Result**: [`src/result/result.ts`](src/result/result.ts) - 27 functions
752
+ - **Composition**: [`src/composition/`](src/composition/) - 6 utilities
753
+ - **Schema**: [`src/schema/`](src/schema/) - 50+ validators
754
+
755
+ ### Examples
756
+
757
+ Working examples organized by category:
758
+
759
+ - **Option**: [`examples/option/option-examples.ts`](examples/option/option-examples.ts)
760
+ - **Result**: [`examples/result/result-examples.ts`](examples/result/result-examples.ts)
761
+ - **Schema**: [`examples/schema/`](examples/schema/)
762
+ - [`basic.ts`](examples/schema/basic.ts) - Basic validators
763
+ - [`union.ts`](examples/schema/union.ts) - Union types
764
+ - **Composition**: [`examples/composition/`](examples/composition/)
765
+ - [`advanced-composition.ts`](examples/composition/advanced-composition.ts)
766
+ - [`curry-basics.ts`](examples/composition/curry-basics.ts)
767
+ - [`tupled-basics.ts`](examples/composition/tupled-basics.ts)
768
+ - **Complete Pipelines**: [`examples/complete-pipelines/`](examples/complete-pipelines/)
769
+ - [`async.ts`](examples/complete-pipelines/async.ts) - Basic async pipeline
770
+ - [`async-launch.ts`](examples/complete-pipelines/async-launch.ts) - Rocket launch decision
771
+ - [`hohmann-transfer.ts`](examples/complete-pipelines/hohmann-transfer.ts) - Orbital mechanics
772
+ - [`hill-clohessy-wiltshire.ts`](examples/complete-pipelines/hill-clohessy-wiltshire.ts) - Spacecraft rendezvous
773
+ - **Interop**: [`examples/interop/interop-examples.ts`](examples/interop/interop-examples.ts)
774
+
775
+ ### Advanced Topics
776
+
777
+ For advanced implementation details:
778
+
779
+ - **Symbol Branding**: How types are protected from structural typing
780
+ - **Tuple-Preserving Combinators**: Type-level magic for `combine()`
781
+ - **Type Inference**: How schema types are extracted
782
+
783
+ See [`docs/ADVANCED.md`](docs/ADVANCED.md)
784
+
785
+ ### Contributing
786
+
787
+ Interested in contributing? See [`CONTRIBUTING.md`](CONTRIBUTING.md) for:
788
+
789
+ - Development setup and commands
790
+ - Project structure
791
+ - Code style guidelines
792
+ - Testing patterns
793
+ - Pull request process
794
+
795
+ ---
796
+
797
+ ## Further Reading
798
+
799
+ - [Railway-Oriented Programming](https://fsharpforfunandprofit.com/rop/) - Original concept by Scott Wlaschin
800
+ - [Parse, Don't Validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) - Type-driven validation
801
+ - [Making Illegal States Unrepresentable](https://ybogomolov.me/making-illegal-states-unrepresentable) - Type-level constraints
802
+
803
+ ---
804
+
805
+ ## License
806
+
807
+ MIT © Sarkis Melkonian
808
+
809
+ ---
810
+
811
+ **Build robust pipelines. Make errors boring. Let data flow.**