@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.
- package/README.md +811 -0
- package/dist/composition/index.cjs +72 -0
- package/dist/composition/index.cjs.map +1 -0
- package/dist/composition/index.d.cts +286 -0
- package/dist/composition/index.d.ts +286 -0
- package/dist/composition/index.mjs +65 -0
- package/dist/composition/index.mjs.map +1 -0
- package/dist/index-BdfKTZ7O.d.cts +799 -0
- package/dist/index-BdfKTZ7O.d.ts +799 -0
- package/dist/index.cjs +1074 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +969 -0
- package/dist/index.mjs.map +1 -0
- package/dist/option/index.cjs +111 -0
- package/dist/option/index.cjs.map +1 -0
- package/dist/option/index.d.cts +1 -0
- package/dist/option/index.d.ts +1 -0
- package/dist/option/index.mjs +93 -0
- package/dist/option/index.mjs.map +1 -0
- package/dist/result/index.cjs +178 -0
- package/dist/result/index.cjs.map +1 -0
- package/dist/result/index.d.cts +1 -0
- package/dist/result/index.d.ts +1 -0
- package/dist/result/index.mjs +152 -0
- package/dist/result/index.mjs.map +1 -0
- package/dist/schema/index.cjs +794 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.cts +1867 -0
- package/dist/schema/index.d.ts +1867 -0
- package/dist/schema/index.mjs +735 -0
- package/dist/schema/index.mjs.map +1 -0
- package/examples/complete-pipelines/async-launch.ts +128 -0
- package/examples/complete-pipelines/async.ts +119 -0
- package/examples/complete-pipelines/hill-clohessy-wiltshire.ts +218 -0
- package/examples/complete-pipelines/hohmann-transfer.ts +159 -0
- package/examples/composition/advanced-composition.ts +32 -0
- package/examples/composition/curry-basics.ts +24 -0
- package/examples/composition/tupled-basics.ts +26 -0
- package/examples/index.ts +47 -0
- package/examples/interop/interop-examples.ts +110 -0
- package/examples/option/option-examples.ts +63 -0
- package/examples/result/result-examples.ts +110 -0
- package/examples/schema/basic.ts +78 -0
- package/examples/schema/union.ts +301 -0
- package/package.json +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
# @railway-ts/pipelines
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@railway-ts/pipelines) [](https://github.com/sakobu/railway-ts-pipelines/actions) [](https://opensource.org/licenses/MIT) [](https://bundlephobia.com/package/@railway-ts/pipelines) [](https://www.typescriptlang.org/) [](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.**
|