@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 +115 -706
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +64 -1
- package/dist/index.mjs.map +1 -1
- package/dist/schema/index.cjs +65 -0
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +64 -2
- package/dist/schema/index.d.ts +64 -2
- package/dist/schema/index.mjs +64 -1
- package/dist/schema/index.mjs.map +1 -1
- package/package.json +17 -13
package/README.md
CHANGED
|
@@ -2,474 +2,112 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
|
|
5
|
+
Railway-oriented programming for TypeScript. Result and Option types that don't suck.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
39
|
+
**The pattern:** Validate at boundaries, chain operations, branch once at the end. Errors propagate automatically.
|
|
305
40
|
|
|
306
|
-
|
|
307
|
-
type Validator<Input, Output = Input> = (value: Input, path?: string[]) => Result<Output, ValidationError[]>;
|
|
41
|
+
## Documentation
|
|
308
42
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
**Type-safe:** Symbol branding prevents duck typing bugs. Tuple preservation means no type casts.
|
|
352
55
|
|
|
353
|
-
|
|
56
|
+
**Railway-oriented:** Errors propagate automatically. Write happy path code, handle errors once at the end.
|
|
354
57
|
|
|
355
|
-
|
|
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
|
-
|
|
367
|
-
validate('15', adultAge); // err([{ path: [], message: "Must be at least 18" }])
|
|
368
|
-
```
|
|
60
|
+
### Option
|
|
369
61
|
|
|
370
|
-
|
|
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
|
-
|
|
394
|
-
const
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
70
|
+
match(name, {
|
|
71
|
+
some: (n) => console.log(n),
|
|
72
|
+
none: () => console.log('No user'),
|
|
73
|
+
});
|
|
415
74
|
```
|
|
416
75
|
|
|
417
|
-
|
|
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
|
-
|
|
83
|
+
### Result
|
|
420
84
|
|
|
421
|
-
|
|
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
|
-
|
|
430
|
-
|
|
87
|
+
```typescript
|
|
88
|
+
import { ok, err, map, flatMap, match } from '@railway-ts/pipelines/result';
|
|
431
89
|
|
|
432
|
-
|
|
90
|
+
const divide = (a: number, b: number) => (b === 0 ? err('div by zero') : ok(a / b));
|
|
433
91
|
|
|
434
|
-
|
|
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
|
-
|
|
444
|
-
|
|
94
|
+
match(result, {
|
|
95
|
+
ok: (value) => console.log(value),
|
|
96
|
+
err: (error) => console.error(error),
|
|
97
|
+
});
|
|
445
98
|
```
|
|
446
99
|
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
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
|
-
|
|
135
|
+
const result = validate(input, userSchema);
|
|
136
|
+
// Result<User, ValidationError[]>
|
|
137
|
+
```
|
|
532
138
|
|
|
533
|
-
|
|
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
|
-
|
|
150
|
+
### Composition
|
|
536
151
|
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
-
// Build function pipelines
|
|
164
|
+
// Build reusable pipeline
|
|
649
165
|
const process = flow(
|
|
650
|
-
(
|
|
651
|
-
(
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
+
Start with `examples/complete-pipelines/async-launch.ts` for a real-world pattern.
|
|
798
213
|
|
|
799
|
-
|
|
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.**
|