@railway-ts/pipelines 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -2,474 +2,112 @@
2
2
 
3
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
4
 
5
- **Make failure boring. Make data flow.**
5
+ Railway-oriented programming for TypeScript. Result and Option types that don't suck.
6
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.
7
+ **Design philosophy:** Small, focused API surface. Practical over academic. No fp-ts complexity, no Effect-TS kitchen sink.
8
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
9
+ ## Install
132
10
 
133
11
  ```bash
134
- bun add @railway-ts/pipelines
135
- # or npm install @railway-ts/pipelines
12
+ bun add @railway-ts/pipelines # or npm, pnpm, yarn
136
13
  ```
137
14
 
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>`.
15
+ Requires TypeScript 5.0+ and Node.js 18+.
168
16
 
169
- #### Core Type
17
+ ## Quick Start
170
18
 
171
19
  ```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
20
  import { pipe } from '@railway-ts/pipelines/composition';
21
+ import { ok, match, andThen } from '@railway-ts/pipelines/result';
22
+ import { validate, object, required, chain, parseNumber, min } from '@railway-ts/pipelines/schema';
239
23
 
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),
24
+ const schema = object({
25
+ x: required(chain(parseNumber(), min(0))),
26
+ y: required(chain(parseNumber(), min(1))),
255
27
  });
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
28
 
279
- ### Schema: Parse, Don't Validate
29
+ async function compute(input: unknown) {
30
+ const result = await pipe(validate(input, schema), (r) => andThen(r, ({ x, y }) => ok(x / y)));
280
31
 
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'
32
+ return match(result, {
33
+ ok: (value) => ({ success: true, value }),
34
+ err: (errors) => ({ success: false, errors }),
35
+ });
293
36
  }
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
37
  ```
303
38
 
304
- #### Core Types
39
+ **The pattern:** Validate at boundaries, chain operations, branch once at the end. Errors propagate automatically.
305
40
 
306
- ```typescript
307
- type Validator<Input, Output = Input> = (value: Input, path?: string[]) => Result<Output, ValidationError[]>;
41
+ ## Documentation
308
42
 
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';
43
+ **[Getting Started](GETTING_STARTED.md)** - Your first pipeline
44
+ **[Recipes](docs/RECIPES.md)** - Common patterns (point-free composition, async, validation)
45
+ → **[Advanced](docs/ADVANCED.md)** - Symbol branding, tuple preservation, type inference
46
+ → **[Examples](examples/)** - Working code you can run
334
47
 
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[]>
48
+ ## Why This Library
339
49
 
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
- });
50
+ **Focused scope:** Result, Option, validation, composition. That's it. No monads seminar.
346
51
 
347
- type User = InferSchemaType<typeof userSchema>;
348
- // { name: string; email?: string; age: number | null }
349
- ```
52
+ **Practical:** Eliminates boilerplate for real patterns. Documentation shows you how, doesn't make it part of the API.
350
53
 
351
- #### Chaining Validators
54
+ **Type-safe:** Symbol branding prevents duck typing bugs. Tuple preservation means no type casts.
352
55
 
353
- Use `chain()` to build validation pipelines:
56
+ **Railway-oriented:** Errors propagate automatically. Write happy path code, handle errors once at the end.
354
57
 
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
- );
58
+ ## API Reference
365
59
 
366
- validate('25', adultAge); // ok(25) - parsed and validated
367
- validate('15', adultAge); // err([{ path: [], message: "Must be at least 18" }])
368
- ```
60
+ ### Option
369
61
 
370
- #### String, Number, Array Validators
62
+ Handle nullable values without `if (x != null)` everywhere.
371
63
 
372
64
  ```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';
65
+ import { some, none, map, flatMap, match } from '@railway-ts/pipelines/option';
392
66
 
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
- );
67
+ const user = some({ name: 'Alice', age: 25 });
68
+ const name = pipe(user, (o) => map(o, (u) => u.name));
411
69
 
412
- // Enum validation
413
- const status = stringEnum(['pending', 'approved', 'rejected'] as const);
414
- // Returns: Validator<unknown, 'pending' | 'approved' | 'rejected'>
70
+ match(name, {
71
+ some: (n) => console.log(n),
72
+ none: () => console.log('No user'),
73
+ });
415
74
  ```
416
75
 
417
- #### Parsing Validators (Type Transformations)
76
+ **Core:** `some`, `none`, `isSome`, `isNone`
77
+ **Transform:** `map`, `flatMap`, `bimap`, `filter`, `tap`
78
+ **Unwrap:** `unwrap`, `unwrapOr`, `unwrapOrElse`
79
+ **Combine:** `combine`
80
+ **Convert:** `fromNullable`, `mapToResult`
81
+ **Branch:** `match`
418
82
 
419
- These validators transform types during validation:
83
+ ### Result
420
84
 
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';
85
+ Explicit error handling. No exceptions, no try-catch pyramids.
428
86
 
429
- // Convert string to number in validation pipeline
430
- const ageFromString = chain(parseNumber('Invalid number'), min(18, 'Must be adult'));
87
+ ```typescript
88
+ import { ok, err, map, flatMap, match } from '@railway-ts/pipelines/result';
431
89
 
432
- validate('25', ageFromString); // ok(25) - note: number, not string
90
+ const divide = (a: number, b: number) => (b === 0 ? err('div by zero') : ok(a / b));
433
91
 
434
- // Parse JSON and validate structure
435
- const jsonUserSchema = chain(
436
- parseJSON(),
437
- object({
438
- name: required(string()),
439
- age: required(number()),
440
- }),
441
- );
92
+ const result = pipe(divide(10, 2), (r) => map(r, (x) => x * 3));
442
93
 
443
- validate('{"name":"Alice","age":30}', jsonUserSchema);
444
- // ok({ name: "Alice", age: 30 })
94
+ match(result, {
95
+ ok: (value) => console.log(value),
96
+ err: (error) => console.error(error),
97
+ });
445
98
  ```
446
99
 
447
- #### Union Types
100
+ **Core:** `ok`, `err`, `isOk`, `isErr`
101
+ **Transform:** `map`, `mapErr`, `flatMap`, `bimap`, `filter`, `tap`, `tapErr`
102
+ **Unwrap:** `unwrap`, `unwrapOr`, `unwrapOrElse`
103
+ **Combine:** `combine`, `combineAll`
104
+ **Convert:** `fromTry`, `fromTryWithError`, `fromPromise`, `fromPromiseWithError`, `toPromise`, `mapToOption`
105
+ **Async:** `andThen`
106
+ **Branch:** `match`
448
107
 
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
- ```
108
+ ### Schema
471
109
 
472
- #### Complete Schema Example
110
+ Parse untrusted data into typed values. Accumulates all validation errors.
473
111
 
474
112
  ```typescript
475
113
  import {
@@ -479,197 +117,63 @@ import {
479
117
  optional,
480
118
  chain,
481
119
  string,
482
- minLength,
483
- maxLength,
484
- email,
485
120
  parseNumber,
486
- integer,
487
121
  min,
488
122
  max,
489
- array,
490
- minItems,
491
- maxItems,
492
- stringEnum,
493
- formatErrors,
494
123
  type InferSchemaType,
495
124
  } from '@railway-ts/pipelines/schema';
496
125
 
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)),
126
+ const userSchema = object({
127
+ name: required(string()),
128
+ age: required(chain(parseNumber(), min(18), max(120))),
129
+ email: optional(string()),
504
130
  });
505
131
 
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
- ---
132
+ type User = InferSchemaType<typeof userSchema>;
133
+ // { name: string; age: number; email?: string }
530
134
 
531
- ### Composition: Build Complex Pipelines
135
+ const result = validate(input, userSchema);
136
+ // Result<User, ValidationError[]>
137
+ ```
532
138
 
533
- Functional composition utilities for building data pipelines.
139
+ **Primitives:** `string`, `number`, `boolean`, `date`, `bigint`
140
+ **Parsers:** `parseNumber`, `parseInt`, `parseFloat`, `parseJSON`, `parseString`, `parseBigInt`, `parseBool`, `parseDate`, `parseISODate`, `parseURL`, `parseEnum`
141
+ **Structures:** `object`, `array`, `tuple`, `tupleOf`
142
+ **Unions:** `union`, `discriminatedUnion`, `literal`
143
+ **Modifiers:** `required`, `optional`, `nullable`, `emptyAsOptional`
144
+ **String Constraints:** `minLength`, `maxLength`, `pattern`, `nonEmpty`, `email`, `phoneNumber`
145
+ **Number Constraints:** `min`, `max`, `integer`, `finite`, `between`
146
+ **Enums:** `stringEnum`, `numberEnum`
147
+ **Combinators:** `chain`, `transform`, `refine`, `matches`
148
+ **Utilities:** `validate`, `formatErrors`, `InferSchemaType`, `Validator`, `ValidationError`
534
149
 
535
- #### pipe() - Immediate Composition
150
+ ### Composition
536
151
 
537
- Left-to-right data flow with immediate execution:
152
+ Build pipelines. No nested function calls.
538
153
 
539
154
  ```typescript
540
- import { pipe } from '@railway-ts/pipelines/composition';
155
+ import { pipe, flow, curry } from '@railway-ts/pipelines/composition';
541
156
 
157
+ // Immediate execution
542
158
  const result = pipe(
543
159
  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
160
  (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**
161
+ (x) => x + 1,
162
+ ); // 11
646
163
 
647
- ```typescript
648
- // Build function pipelines
164
+ // Build reusable pipeline
649
165
  const process = flow(
650
- (input) => validate(input, schema),
651
- (r) => andThen(r, fetchData),
652
- (r) => andThen(r, transform),
166
+ (x: number) => x * 2,
167
+ (x) => x + 1,
653
168
  );
169
+ process(5); // 11
654
170
  ```
655
171
 
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
- ```
172
+ **Functions:** `pipe`, `flow`, `curry`, `uncurry`, `tupled`, `untupled`
665
173
 
666
- ---
174
+ ## Import Patterns
667
175
 
668
- ## Import Strategy
669
-
670
- The library provides **tree-shakable subpath imports**:
671
-
672
- ### Recommended: Subpath Imports
176
+ ### Subpath imports (recommended for tree-shaking)
673
177
 
674
178
  ```typescript
675
179
  import { some, none, map } from '@railway-ts/pipelines/option';
@@ -678,134 +182,39 @@ import { pipe, flow } from '@railway-ts/pipelines/composition';
678
182
  import { string, number, validate } from '@railway-ts/pipelines/schema';
679
183
  ```
680
184
 
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
185
+ ### Root imports (adds type suffixes)
688
186
 
689
187
  ```typescript
690
- import {
691
- mapOption, // Option's map
692
- mapResult, // Result's map
693
- pipe,
694
- ok,
695
- validate,
696
- } from '@railway-ts/pipelines';
188
+ import { mapOption, mapResult, pipe, ok, validate } from '@railway-ts/pipelines';
697
189
  ```
698
190
 
699
- **When to use:** Convenience when you need functions from multiple modules and don't mind renamed functions.
700
-
701
- **Module structure:**
191
+ Functions that exist in both Result and Option get suffixes when imported from root: `mapResult`, `mapOption`, etc. Result-only functions stay unsuffixed: `mapErr`, `andThen`.
702
192
 
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` |
193
+ ## Examples
709
194
 
710
- ---
195
+ Clone and run:
711
196
 
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:
197
+ ```bash
198
+ git clone https://github.com/sakobu/railway-ts-pipelines.git
199
+ cd railway-ts-pipelines
200
+ bun install
201
+ bun run examples/index.ts
202
+ ```
788
203
 
789
- - Development setup and commands
790
- - Project structure
791
- - Code style guidelines
792
- - Testing patterns
793
- - Pull request process
204
+ **What's in there:**
794
205
 
795
- ---
206
+ - `option/` - Nullable handling patterns
207
+ - `result/` - Error handling patterns
208
+ - `schema/` - Validation (basic, unions, tuples)
209
+ - `composition/` - Function composition techniques
210
+ - `complete-pipelines/` - Full examples with validation + async + logic
796
211
 
797
- ## Further Reading
212
+ Start with `examples/complete-pipelines/async-launch.ts` for a real-world pattern.
798
213
 
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
214
+ ## Contributing
802
215
 
803
- ---
216
+ [CONTRIBUTING.md](CONTRIBUTING.md)
804
217
 
805
218
  ## License
806
219
 
807
220
  MIT © Sarkis Melkonian
808
-
809
- ---
810
-
811
- **Build robust pipelines. Make errors boring. Let data flow.**