@railway-ts/pipelines 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -752
- package/package.json +17 -13
package/README.md
CHANGED
|
@@ -2,523 +2,126 @@
|
|
|
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
|
|
15
|
+
Requires TypeScript 5.0+ and Node.js 18+.
|
|
143
16
|
|
|
144
|
-
|
|
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
|
|
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';
|
|
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
|
|
21
|
+
import { ok, match, andThen } from '@railway-ts/pipelines/result';
|
|
321
22
|
import {
|
|
322
23
|
validate,
|
|
323
|
-
string,
|
|
324
|
-
number,
|
|
325
|
-
boolean,
|
|
326
|
-
date,
|
|
327
24
|
object,
|
|
328
25
|
required,
|
|
329
|
-
optional,
|
|
330
|
-
nullable,
|
|
331
26
|
chain,
|
|
332
|
-
|
|
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,
|
|
27
|
+
parseNumber,
|
|
381
28
|
min,
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
positive,
|
|
386
|
-
array,
|
|
387
|
-
minItems,
|
|
388
|
-
maxItems,
|
|
389
|
-
unique,
|
|
390
|
-
stringEnum,
|
|
29
|
+
formatErrors,
|
|
30
|
+
type ValidationError,
|
|
31
|
+
type ValidationResult,
|
|
391
32
|
} from '@railway-ts/pipelines/schema';
|
|
392
33
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
minLength(3, 'Too short'),
|
|
398
|
-
pattern(/^[a-zA-Z0-9_]+$/, 'Alphanumeric only'),
|
|
399
|
-
);
|
|
34
|
+
const schema = object({
|
|
35
|
+
x: required(chain(parseNumber(), min(0))),
|
|
36
|
+
y: required(chain(parseNumber(), min(1))),
|
|
37
|
+
});
|
|
400
38
|
|
|
401
|
-
|
|
402
|
-
const
|
|
39
|
+
async function compute(input: unknown): Promise<ValidationResult<number>> {
|
|
40
|
+
const result = await pipe(validate(input, schema), (r) => andThen(r, ({ x, y }) => ok(x / y)));
|
|
403
41
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
unique('Duplicate tags not allowed'),
|
|
410
|
-
);
|
|
42
|
+
return match<number, ValidationError[], ValidationResult<number>>(result, {
|
|
43
|
+
ok: (value) => ({ valid: true, data: value }),
|
|
44
|
+
err: (errors) => ({ valid: false, errors: formatErrors(errors) }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
411
47
|
|
|
412
|
-
//
|
|
413
|
-
const status = stringEnum(['pending', 'approved', 'rejected'] as const);
|
|
414
|
-
// Returns: Validator<unknown, 'pending' | 'approved' | 'rejected'>
|
|
48
|
+
await compute({ x: 10, y: 2 }).then(console.log); // { valid: true, data: 5 }
|
|
415
49
|
```
|
|
416
50
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
These validators transform types during validation:
|
|
51
|
+
**The pattern:** Validate at boundaries, chain operations, branch once at the end. Errors propagate automatically.
|
|
420
52
|
|
|
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';
|
|
53
|
+
## Documentation
|
|
428
54
|
|
|
429
|
-
|
|
430
|
-
|
|
55
|
+
→ **[Getting Started](GETTING_STARTED.md)** - Your first pipeline
|
|
56
|
+
→ **[Recipes](docs/RECIPES.md)** - Common patterns (point-free composition, async, validation)
|
|
57
|
+
→ **[Advanced](docs/ADVANCED.md)** - Symbol branding, tuple preservation, type inference
|
|
58
|
+
→ **[Examples](examples/)** - Working code you can run
|
|
431
59
|
|
|
432
|
-
|
|
60
|
+
## Why This Library
|
|
433
61
|
|
|
434
|
-
|
|
435
|
-
const jsonUserSchema = chain(
|
|
436
|
-
parseJSON(),
|
|
437
|
-
object({
|
|
438
|
-
name: required(string()),
|
|
439
|
-
age: required(number()),
|
|
440
|
-
}),
|
|
441
|
-
);
|
|
62
|
+
**Focused scope:** Result, Option, validation, composition. That's it. No monads seminar.
|
|
442
63
|
|
|
443
|
-
|
|
444
|
-
// ok({ name: "Alice", age: 30 })
|
|
445
|
-
```
|
|
64
|
+
**Practical:** Eliminates boilerplate for real patterns. Documentation shows you how, doesn't make it part of the API.
|
|
446
65
|
|
|
447
|
-
|
|
66
|
+
**Type-safe:** Symbol branding prevents duck typing bugs. Tuple preservation means no type casts.
|
|
448
67
|
|
|
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
|
-
});
|
|
68
|
+
**Railway-oriented:** Errors propagate automatically. Write happy path code, handle errors once at the end.
|
|
467
69
|
|
|
468
|
-
|
|
469
|
-
// { type: 'circle', radius: number } | { type: 'rectangle', width: number, height: number }
|
|
470
|
-
```
|
|
70
|
+
## API Reference
|
|
471
71
|
|
|
472
|
-
|
|
72
|
+
### Option
|
|
473
73
|
|
|
474
|
-
|
|
74
|
+
Handle nullable values without `if (x != null)` everywhere.
|
|
475
75
|
|
|
476
76
|
```typescript
|
|
477
|
-
import {
|
|
478
|
-
|
|
479
|
-
// Heterogeneous tuple - different types per position
|
|
480
|
-
const userRecord = tuple([
|
|
481
|
-
string(), // position 0: string
|
|
482
|
-
chain(number(), integer(), min(0)), // position 1: non-negative integer
|
|
483
|
-
boolean(), // position 2: boolean
|
|
484
|
-
]);
|
|
77
|
+
import { pipe } from '@railway-ts/pipelines/composition';
|
|
78
|
+
import { some, map, match } from '@railway-ts/pipelines/option';
|
|
485
79
|
|
|
486
|
-
|
|
487
|
-
|
|
80
|
+
const user = some({ name: 'Alice', age: 25 });
|
|
81
|
+
const name = pipe(user, (o) => map(o, (u) => u.name));
|
|
488
82
|
|
|
489
|
-
|
|
490
|
-
|
|
83
|
+
match(name, {
|
|
84
|
+
some: (n) => console.log(n),
|
|
85
|
+
none: () => console.log('No user'),
|
|
86
|
+
}); // Output: Alice
|
|
87
|
+
```
|
|
491
88
|
|
|
492
|
-
|
|
493
|
-
|
|
89
|
+
**Core:** `some`, `none`, `isSome`, `isNone`
|
|
90
|
+
**Transform:** `map`, `flatMap`, `bimap`, `filter`, `tap`
|
|
91
|
+
**Unwrap:** `unwrap`, `unwrapOr`, `unwrapOrElse`
|
|
92
|
+
**Combine:** `combine`
|
|
93
|
+
**Convert:** `fromNullable`, `mapToResult`
|
|
94
|
+
**Branch:** `match`
|
|
494
95
|
|
|
495
|
-
|
|
496
|
-
// [number, number, number]
|
|
96
|
+
### Result
|
|
497
97
|
|
|
498
|
-
|
|
499
|
-
validate([1, 2], vector3); // err - expected length 3, got 2
|
|
98
|
+
Explicit error handling. No exceptions, no try-catch pyramids.
|
|
500
99
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
3
|
|
505
|
-
);
|
|
100
|
+
```typescript
|
|
101
|
+
import { pipe } from '@railway-ts/pipelines/composition';
|
|
102
|
+
import { ok, err, map, match } from '@railway-ts/pipelines/result';
|
|
506
103
|
|
|
507
|
-
|
|
508
|
-
validate([255, 300, 0], rgbColor); // err - 300 exceeds max of 255
|
|
104
|
+
const divide = (a: number, b: number) => (b === 0 ? err('div by zero') : ok(a / b));
|
|
509
105
|
|
|
510
|
-
|
|
511
|
-
const coordinate = tuple([
|
|
512
|
-
chain(number(), min(-90), max(90)), // latitude
|
|
513
|
-
chain(number(), min(-180), max(180)), // longitude
|
|
514
|
-
]);
|
|
106
|
+
const result = pipe(divide(10, 2), (r) => map(r, (x) => x * 3));
|
|
515
107
|
|
|
516
|
-
|
|
108
|
+
match(result, {
|
|
109
|
+
ok: (value) => console.log(value),
|
|
110
|
+
err: (error) => console.error(error),
|
|
111
|
+
}); // Output: 15
|
|
517
112
|
```
|
|
518
113
|
|
|
519
|
-
**
|
|
114
|
+
**Core:** `ok`, `err`, `isOk`, `isErr`
|
|
115
|
+
**Transform:** `map`, `mapErr`, `flatMap`, `bimap`, `filter`, `tap`, `tapErr`
|
|
116
|
+
**Unwrap:** `unwrap`, `unwrapOr`, `unwrapOrElse`
|
|
117
|
+
**Combine:** `combine`, `combineAll`
|
|
118
|
+
**Convert:** `fromTry`, `fromTryWithError`, `fromPromise`, `fromPromiseWithError`, `toPromise`, `mapToOption`
|
|
119
|
+
**Async:** `andThen`
|
|
120
|
+
**Branch:** `match`
|
|
520
121
|
|
|
521
|
-
|
|
122
|
+
### Schema
|
|
123
|
+
|
|
124
|
+
Parse untrusted data into typed values. Accumulates all validation errors.
|
|
522
125
|
|
|
523
126
|
```typescript
|
|
524
127
|
import {
|
|
@@ -528,197 +131,63 @@ import {
|
|
|
528
131
|
optional,
|
|
529
132
|
chain,
|
|
530
133
|
string,
|
|
531
|
-
minLength,
|
|
532
|
-
maxLength,
|
|
533
|
-
email,
|
|
534
134
|
parseNumber,
|
|
535
|
-
integer,
|
|
536
135
|
min,
|
|
537
136
|
max,
|
|
538
|
-
array,
|
|
539
|
-
minItems,
|
|
540
|
-
maxItems,
|
|
541
|
-
stringEnum,
|
|
542
|
-
formatErrors,
|
|
543
137
|
type InferSchemaType,
|
|
544
138
|
} from '@railway-ts/pipelines/schema';
|
|
545
139
|
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
age: required(chain(parseNumber(), integer(), min(13), max(120))),
|
|
551
|
-
interests: required(chain(array(string()), minItems(1), maxItems(10))),
|
|
552
|
-
role: required(stringEnum(['user', 'admin', 'moderator'] as const)),
|
|
140
|
+
const userSchema = object({
|
|
141
|
+
name: required(string()),
|
|
142
|
+
age: required(chain(parseNumber(), min(18), max(120))),
|
|
143
|
+
email: optional(string()),
|
|
553
144
|
});
|
|
554
145
|
|
|
555
|
-
type
|
|
556
|
-
// {
|
|
557
|
-
// username: string;
|
|
558
|
-
// email: string;
|
|
559
|
-
// bio?: string;
|
|
560
|
-
// age: number;
|
|
561
|
-
// interests: string[];
|
|
562
|
-
// role: 'user' | 'admin' | 'moderator';
|
|
563
|
-
// }
|
|
564
|
-
|
|
565
|
-
function createUser(input: unknown): Result<User, Record<string, string>> {
|
|
566
|
-
const validated = validate(input, createUserSchema);
|
|
567
|
-
|
|
568
|
-
return match(validated, {
|
|
569
|
-
ok: (data) => saveToDatabase(data), // data is CreateUserInput
|
|
570
|
-
err: (errors) => err(formatErrors(errors)),
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
**See full validator catalog**: [`src/schema/`](src/schema/) (50+ validators)
|
|
576
|
-
**See examples**: [`examples/schema/`](examples/schema/)
|
|
577
|
-
|
|
578
|
-
---
|
|
146
|
+
type User = InferSchemaType<typeof userSchema>;
|
|
147
|
+
// { name: string; age: number; email?: string }
|
|
579
148
|
|
|
580
|
-
|
|
149
|
+
const result = validate(input, userSchema);
|
|
150
|
+
// Result<User, ValidationError[]>
|
|
151
|
+
```
|
|
581
152
|
|
|
582
|
-
|
|
153
|
+
**Primitives:** `string`, `number`, `boolean`, `date`, `bigint`
|
|
154
|
+
**Parsers:** `parseNumber`, `parseInt`, `parseFloat`, `parseJSON`, `parseString`, `parseBigInt`, `parseBool`, `parseDate`, `parseISODate`, `parseURL`, `parseEnum`
|
|
155
|
+
**Structures:** `object`, `array`, `tuple`, `tupleOf`
|
|
156
|
+
**Unions:** `union`, `discriminatedUnion`, `literal`
|
|
157
|
+
**Modifiers:** `required`, `optional`, `nullable`, `emptyAsOptional`
|
|
158
|
+
**String Constraints:** `minLength`, `maxLength`, `pattern`, `nonEmpty`, `email`, `phoneNumber`
|
|
159
|
+
**Number Constraints:** `min`, `max`, `integer`, `finite`, `between`
|
|
160
|
+
**Enums:** `stringEnum`, `numberEnum`
|
|
161
|
+
**Combinators:** `chain`, `transform`, `refine`, `matches`
|
|
162
|
+
**Utilities:** `validate`, `formatErrors`, `InferSchemaType`, `Validator`, `ValidationError`
|
|
583
163
|
|
|
584
|
-
|
|
164
|
+
### Composition
|
|
585
165
|
|
|
586
|
-
|
|
166
|
+
Build pipelines. No nested function calls.
|
|
587
167
|
|
|
588
168
|
```typescript
|
|
589
|
-
import { pipe } from '@railway-ts/pipelines/composition';
|
|
169
|
+
import { pipe, flow, curry } from '@railway-ts/pipelines/composition';
|
|
590
170
|
|
|
171
|
+
// Immediate execution
|
|
591
172
|
const result = pipe(
|
|
592
173
|
5,
|
|
593
|
-
(x) => x * 2, // 10
|
|
594
|
-
(x) => x + 1, // 11
|
|
595
|
-
(x) => String(x), // "11"
|
|
596
|
-
);
|
|
597
|
-
// "11"
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
Supports up to 10 steps with full type inference at each stage.
|
|
601
|
-
|
|
602
|
-
#### flow() - Deferred Composition
|
|
603
|
-
|
|
604
|
-
Build reusable function pipelines:
|
|
605
|
-
|
|
606
|
-
```typescript
|
|
607
|
-
import { flow } from '@railway-ts/pipelines/composition';
|
|
608
|
-
|
|
609
|
-
const processNumber = flow(
|
|
610
|
-
(x: number) => x * 2,
|
|
611
|
-
(x) => x + 1,
|
|
612
|
-
(x) => String(x),
|
|
613
|
-
);
|
|
614
|
-
|
|
615
|
-
processNumber(5); // "11"
|
|
616
|
-
processNumber(10); // "21"
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
First function can accept multiple arguments:
|
|
620
|
-
|
|
621
|
-
```typescript
|
|
622
|
-
const processSum = flow(
|
|
623
|
-
(a: number, b: number) => a + b,
|
|
624
174
|
(x) => x * 2,
|
|
625
|
-
|
|
626
|
-
);
|
|
627
|
-
|
|
628
|
-
processSum(3, 4); // "14"
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
#### curry() / tupled()
|
|
632
|
-
|
|
633
|
-
Convert between different function forms:
|
|
634
|
-
|
|
635
|
-
```typescript
|
|
636
|
-
import { curry, tupled } from '@railway-ts/pipelines/composition';
|
|
637
|
-
|
|
638
|
-
const add = (a: number, b: number) => a + b;
|
|
639
|
-
|
|
640
|
-
// Currying for partial application
|
|
641
|
-
const curriedAdd = curry(add);
|
|
642
|
-
curriedAdd(5)(3); // 8
|
|
643
|
-
|
|
644
|
-
pipe(5, curry(add)(10)); // 15
|
|
645
|
-
|
|
646
|
-
// Tupled for array destructuring
|
|
647
|
-
const tupledAdd = tupled(add);
|
|
648
|
-
tupledAdd([5, 3]); // 8
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
**See full API**: [`src/composition/`](src/composition/)
|
|
652
|
-
**See examples**: [`examples/composition/`](examples/composition/)
|
|
653
|
-
|
|
654
|
-
---
|
|
655
|
-
|
|
656
|
-
## Quick Reference
|
|
657
|
-
|
|
658
|
-
### When to Use What
|
|
659
|
-
|
|
660
|
-
| Scenario | Use | Why |
|
|
661
|
-
| --------------------------- | ----------------- | ------------------------------------- |
|
|
662
|
-
| Value might be absent | `Option<T>` | Absence is normal, not exceptional |
|
|
663
|
-
| Operation can fail | `Result<T, E>` | Need to communicate why it failed |
|
|
664
|
-
| Validating untrusted input | Schema validators | Parse into guaranteed-valid types |
|
|
665
|
-
| Building data flows | `pipe`, `flow` | Compose transformations left-to-right |
|
|
666
|
-
| Multi-step async operations | `andThen` | Chain async Results without nesting |
|
|
667
|
-
|
|
668
|
-
### Common Patterns
|
|
669
|
-
|
|
670
|
-
**Boundary Validation**
|
|
671
|
-
|
|
672
|
-
```typescript
|
|
673
|
-
// unknown -> Result<T, E>
|
|
674
|
-
const result = validate(untrustedInput, schema);
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
**Transform on Success Path**
|
|
678
|
-
|
|
679
|
-
```typescript
|
|
680
|
-
// Stay on the rails
|
|
681
|
-
const transformed = andThen(result, (data) => processData(data));
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
**Branch at the End**
|
|
685
|
-
|
|
686
|
-
```typescript
|
|
687
|
-
// Handle both paths once
|
|
688
|
-
match(result, {
|
|
689
|
-
ok: (data) => handleSuccess(data),
|
|
690
|
-
err: (errors) => handleErrors(errors),
|
|
691
|
-
});
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
**Compose Reusable Pipelines**
|
|
175
|
+
(x) => x + 1,
|
|
176
|
+
); // 11
|
|
695
177
|
|
|
696
|
-
|
|
697
|
-
// Build function pipelines
|
|
178
|
+
// Build reusable pipeline
|
|
698
179
|
const process = flow(
|
|
699
|
-
(
|
|
700
|
-
(
|
|
701
|
-
(r) => andThen(r, transform),
|
|
180
|
+
(x: number) => x * 2,
|
|
181
|
+
(x) => x + 1,
|
|
702
182
|
);
|
|
183
|
+
process(5); // 11
|
|
703
184
|
```
|
|
704
185
|
|
|
705
|
-
**
|
|
186
|
+
**Functions:** `pipe`, `flow`, `curry`, `uncurry`, `tupled`, `untupled`
|
|
706
187
|
|
|
707
|
-
|
|
708
|
-
// All-or-nothing with Option
|
|
709
|
-
const combined = combineOption([some(1), some(2), some(3)]); // Some([1,2,3])
|
|
710
|
-
|
|
711
|
-
// Fail-fast with Result
|
|
712
|
-
const results = combineResult([ok(1), ok(2), ok(3)]); // Ok([1,2,3])
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
---
|
|
188
|
+
## Import Patterns
|
|
716
189
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
The library provides **tree-shakable subpath imports**:
|
|
720
|
-
|
|
721
|
-
### Recommended: Subpath Imports
|
|
190
|
+
### Subpath imports (recommended for tree-shaking)
|
|
722
191
|
|
|
723
192
|
```typescript
|
|
724
193
|
import { some, none, map } from '@railway-ts/pipelines/option';
|
|
@@ -727,135 +196,39 @@ import { pipe, flow } from '@railway-ts/pipelines/composition';
|
|
|
727
196
|
import { string, number, validate } from '@railway-ts/pipelines/schema';
|
|
728
197
|
```
|
|
729
198
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
- Optimal tree-shaking
|
|
733
|
-
- Natural function names (both Option and Result have `map`, no conflicts)
|
|
734
|
-
- Import only what you need
|
|
735
|
-
|
|
736
|
-
### Alternative: Root Import
|
|
199
|
+
### Root imports (adds type suffixes)
|
|
737
200
|
|
|
738
201
|
```typescript
|
|
739
|
-
import {
|
|
740
|
-
mapOption, // Option's map
|
|
741
|
-
mapResult, // Result's map
|
|
742
|
-
pipe,
|
|
743
|
-
ok,
|
|
744
|
-
validate,
|
|
745
|
-
} from '@railway-ts/pipelines';
|
|
202
|
+
import { mapOption, mapResult, pipe, ok, validate } from '@railway-ts/pipelines';
|
|
746
203
|
```
|
|
747
204
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
**Module structure:**
|
|
751
|
-
|
|
752
|
-
| Import Path | Module | Key Types |
|
|
753
|
-
| ----------------------------------- | -------------------- | ----------------- |
|
|
754
|
-
| `@railway-ts/pipelines/option` | Optional values | `Option<T>` |
|
|
755
|
-
| `@railway-ts/pipelines/result` | Fallible operations | `Result<T, E>` |
|
|
756
|
-
| `@railway-ts/pipelines/schema` | Validation | `Validator<I, O>` |
|
|
757
|
-
| `@railway-ts/pipelines/composition` | Function composition | `pipe`, `flow` |
|
|
758
|
-
|
|
759
|
-
---
|
|
760
|
-
|
|
761
|
-
## Is This Right For You?
|
|
762
|
-
|
|
763
|
-
### When to Use railway-ts/pipelines
|
|
764
|
-
|
|
765
|
-
**Choose railway-ts/pipelines when:**
|
|
766
|
-
|
|
767
|
-
- Building data transformation pipelines
|
|
768
|
-
- You want validation + error handling + composition in one integrated system
|
|
769
|
-
- You prefer functional composition over method chaining
|
|
770
|
-
- You need explicit error propagation through multi-step workflows
|
|
771
|
-
- You want a gentler FP learning curve than fp-ts or Effect-TS
|
|
772
|
-
|
|
773
|
-
### When to Choose Something Else
|
|
774
|
-
|
|
775
|
-
**Choose an alternative when:**
|
|
776
|
-
|
|
777
|
-
- You only need validation (Zod is simpler for just validation)
|
|
778
|
-
- You need a full effect system with DI and resource management (Effect-TS)
|
|
779
|
-
- You want comprehensive category theory abstractions (fp-ts)
|
|
780
|
-
- You prefer OOP or method chaining style
|
|
781
|
-
|
|
782
|
-
### Quick Comparison
|
|
783
|
-
|
|
784
|
-
| Library | Scope | Philosophy | Best For |
|
|
785
|
-
| -------------- | ---------------------------- | ------------------------------- | ---------------------------- |
|
|
786
|
-
| **railway-ts** | Railway pattern + validation | Railway-oriented programming | Data pipelines, pragmatic FP |
|
|
787
|
-
| **Zod** | Validation only | Declarative schemas | Drop-in validation |
|
|
788
|
-
| **fp-ts** | Complete FP toolkit | Category theory | FP purists, complex apps |
|
|
789
|
-
| **Effect-TS** | Full effect system | Effect/Fiber/Layer architecture | Large apps, microservices |
|
|
790
|
-
|
|
791
|
-
---
|
|
792
|
-
|
|
793
|
-
## Next Steps
|
|
794
|
-
|
|
795
|
-
### API Reference
|
|
205
|
+
Functions that exist in both Result and Option get suffixes when imported from root: `mapResult`, `mapOption`, etc. Result-only functions stay unsuffixed: `mapErr`, `andThen`.
|
|
796
206
|
|
|
797
|
-
|
|
207
|
+
## Examples
|
|
798
208
|
|
|
799
|
-
|
|
800
|
-
- **Result**: [`src/result/result.ts`](src/result/result.ts) - 27 functions
|
|
801
|
-
- **Composition**: [`src/composition/`](src/composition/) - 6 utilities
|
|
802
|
-
- **Schema**: [`src/schema/`](src/schema/) - 50+ validators
|
|
209
|
+
Clone and run:
|
|
803
210
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
- **Schema**: [`examples/schema/`](examples/schema/)
|
|
811
|
-
- [`basic.ts`](examples/schema/basic.ts) - Basic validators
|
|
812
|
-
- [`union.ts`](examples/schema/union.ts) - Union types
|
|
813
|
-
- [`tuple.ts`](examples/schema/tuple.ts) - Tuple validation
|
|
814
|
-
- **Composition**: [`examples/composition/`](examples/composition/)
|
|
815
|
-
- [`advanced-composition.ts`](examples/composition/advanced-composition.ts)
|
|
816
|
-
- [`curry-basics.ts`](examples/composition/curry-basics.ts)
|
|
817
|
-
- [`tupled-basics.ts`](examples/composition/tupled-basics.ts)
|
|
818
|
-
- **Complete Pipelines**: [`examples/complete-pipelines/`](examples/complete-pipelines/)
|
|
819
|
-
- [`async.ts`](examples/complete-pipelines/async.ts) - Basic async pipeline
|
|
820
|
-
- [`async-launch.ts`](examples/complete-pipelines/async-launch.ts) - Rocket launch decision
|
|
821
|
-
- [`hohmann-transfer.ts`](examples/complete-pipelines/hohmann-transfer.ts) - Orbital mechanics
|
|
822
|
-
- [`hill-clohessy-wiltshire.ts`](examples/complete-pipelines/hill-clohessy-wiltshire.ts) - Spacecraft rendezvous
|
|
823
|
-
- **Interop**: [`examples/interop/interop-examples.ts`](examples/interop/interop-examples.ts)
|
|
824
|
-
|
|
825
|
-
### Advanced Topics
|
|
826
|
-
|
|
827
|
-
For advanced implementation details:
|
|
828
|
-
|
|
829
|
-
- **Symbol Branding**: How types are protected from structural typing
|
|
830
|
-
- **Tuple-Preserving Combinators**: Type-level magic for `combine()`
|
|
831
|
-
- **Type Inference**: How schema types are extracted
|
|
832
|
-
|
|
833
|
-
See [`docs/ADVANCED.md`](docs/ADVANCED.md)
|
|
834
|
-
|
|
835
|
-
### Contributing
|
|
836
|
-
|
|
837
|
-
Interested in contributing? See [`CONTRIBUTING.md`](CONTRIBUTING.md) for:
|
|
211
|
+
```bash
|
|
212
|
+
git clone https://github.com/sakobu/railway-ts-pipelines.git
|
|
213
|
+
cd railway-ts-pipelines
|
|
214
|
+
bun install
|
|
215
|
+
bun run examples/index.ts
|
|
216
|
+
```
|
|
838
217
|
|
|
839
|
-
|
|
840
|
-
- Project structure
|
|
841
|
-
- Code style guidelines
|
|
842
|
-
- Testing patterns
|
|
843
|
-
- Pull request process
|
|
218
|
+
**What's in there:**
|
|
844
219
|
|
|
845
|
-
|
|
220
|
+
- `option/` - Nullable handling patterns
|
|
221
|
+
- `result/` - Error handling patterns
|
|
222
|
+
- `schema/` - Validation (basic, unions, tuples)
|
|
223
|
+
- `composition/` - Function composition techniques
|
|
224
|
+
- `complete-pipelines/` - Full examples with validation + async + logic
|
|
846
225
|
|
|
847
|
-
|
|
226
|
+
Start with `examples/complete-pipelines/async-launch.ts` for a real-world pattern.
|
|
848
227
|
|
|
849
|
-
|
|
850
|
-
- [Parse, Don't Validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) - Type-driven validation
|
|
851
|
-
- [Making Illegal States Unrepresentable](https://ybogomolov.me/making-illegal-states-unrepresentable) - Type-level constraints
|
|
228
|
+
## Contributing
|
|
852
229
|
|
|
853
|
-
|
|
230
|
+
[CONTRIBUTING.md](CONTRIBUTING.md)
|
|
854
231
|
|
|
855
232
|
## License
|
|
856
233
|
|
|
857
234
|
MIT © Sarkis Melkonian
|
|
858
|
-
|
|
859
|
-
---
|
|
860
|
-
|
|
861
|
-
**Build robust pipelines. Make errors boring. Let data flow.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@railway-ts/pipelines",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Functional programming abstractions for TypeScript: Build robust data pipelines with schema validation, Option, Result, and railway-oriented programming.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -34,8 +34,13 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
-
"dist"
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md"
|
|
38
39
|
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
39
44
|
"scripts": {
|
|
40
45
|
"build": "bunx tsup",
|
|
41
46
|
"build:watch": "bunx tsup --watch",
|
|
@@ -67,30 +72,29 @@
|
|
|
67
72
|
"schema",
|
|
68
73
|
"pipeline"
|
|
69
74
|
],
|
|
70
|
-
"repository":
|
|
71
|
-
"type": "git",
|
|
72
|
-
"url": "git+https://github.com/sakobu/railway-ts-pipelines.git"
|
|
73
|
-
},
|
|
75
|
+
"repository": "github:sakobu/railway-ts-pipelines",
|
|
74
76
|
"homepage": "https://github.com/sakobu/railway-ts-pipelines#readme",
|
|
75
77
|
"bugs": {
|
|
76
78
|
"url": "https://github.com/sakobu/railway-ts-pipelines/issues"
|
|
77
79
|
},
|
|
78
|
-
"sideEffects": false,
|
|
79
80
|
"author": "Sarkis Melkonian",
|
|
80
81
|
"license": "MIT",
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
81
85
|
"devDependencies": {
|
|
82
|
-
"@eslint/js": "^9.
|
|
83
|
-
"@types/bun": "
|
|
84
|
-
"eslint": "^9.
|
|
86
|
+
"@eslint/js": "^9.39.1",
|
|
87
|
+
"@types/bun": "1.3.2",
|
|
88
|
+
"eslint": "^9.39.1",
|
|
85
89
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
86
90
|
"eslint-plugin-import": "^2.32.0",
|
|
87
91
|
"eslint-plugin-prettier": "^5.5.4",
|
|
88
92
|
"eslint-plugin-security": "^3.0.1",
|
|
89
|
-
"eslint-plugin-unicorn": "^
|
|
90
|
-
"globals": "^16.
|
|
93
|
+
"eslint-plugin-unicorn": "^62.0.0",
|
|
94
|
+
"globals": "^16.5.0",
|
|
91
95
|
"prettier": "^3.6.2",
|
|
92
96
|
"tsup": "^8.5.0",
|
|
93
|
-
"typescript-eslint": "^8.
|
|
97
|
+
"typescript-eslint": "^8.46.4"
|
|
94
98
|
},
|
|
95
99
|
"peerDependencies": {
|
|
96
100
|
"typescript": "^5"
|