@railway-ts/pipelines
v0.1.7
Published
Railway-oriented pipelines for TypeScript: a lightweight fp‑style Result/Option and schema layer for handling errors and validating boundaries.
Downloads
309
Maintainers
Readme
@railway-ts/pipelines
Railway-oriented programming for TypeScript. Result and Option types that don't suck.
Design philosophy: Small, focused API surface. Practical over academic. No fp-ts complexity, no Effect-TS kitchen sink.
Install
bun add @railway-ts/pipelines # or npm, pnpm, yarnRequires TypeScript 5.0+ and Node.js 18+.
Quick Start
import { pipe } from '@railway-ts/pipelines/composition';
import { ok, match, andThen } from '@railway-ts/pipelines/result';
import {
validate,
object,
required,
chain,
parseNumber,
min,
formatErrors,
type ValidationError,
type ValidationResult,
} from '@railway-ts/pipelines/schema';
const schema = object({
x: required(chain(parseNumber(), min(0))),
y: required(chain(parseNumber(), min(1))),
});
async function compute(input: unknown): Promise<ValidationResult<number>> {
const result = await pipe(validate(input, schema), (r) => andThen(r, ({ x, y }) => ok(x / y)));
return match<number, ValidationError[], ValidationResult<number>>(result, {
ok: (value) => ({ valid: true, data: value }),
err: (errors) => ({ valid: false, errors: formatErrors(errors) }),
});
}
await compute({ x: 10, y: 2 }).then(console.log); // { valid: true, data: 5 }The pattern: Validate at boundaries, chain operations, branch once at the end. Errors propagate automatically.
Documentation
→ Getting Started - Your first pipeline
→ Recipes - Common patterns (point-free composition, async, validation)
→ Advanced - Symbol branding, tuple preservation, type inference
→ Examples - Working code you can run
Why This Library
Focused scope: Result, Option, validation, composition. That's it. No monads seminar.
Practical: Eliminates boilerplate for real patterns. Documentation shows you how, doesn't make it part of the API.
Type-safe: Symbol branding prevents duck typing bugs. Tuple preservation means no type casts.
Railway-oriented: Errors propagate automatically. Write happy path code, handle errors once at the end.
API Reference
Option
Handle nullable values without if (x != null) everywhere.
import { pipe } from '@railway-ts/pipelines/composition';
import { some, map, match } from '@railway-ts/pipelines/option';
const user = some({ name: 'Alice', age: 25 });
const name = pipe(user, (o) => map(o, (u) => u.name));
match(name, {
some: (n) => console.log(n),
none: () => console.log('No user'),
}); // Output: AliceCore: some, none, isSome, isNone
Transform: map, flatMap, bimap, filter, tap
Unwrap: unwrap, unwrapOr, unwrapOrElse
Combine: combine
Convert: fromNullable, mapToResult
Branch: match
Result
Explicit error handling. No exceptions, no try-catch pyramids.
import { pipe } from '@railway-ts/pipelines/composition';
import { ok, err, map, match } from '@railway-ts/pipelines/result';
const divide = (a: number, b: number) => (b === 0 ? err('div by zero') : ok(a / b));
const result = pipe(divide(10, 2), (r) => map(r, (x) => x * 3));
match(result, {
ok: (value) => console.log(value),
err: (error) => console.error(error),
}); // Output: 15Core: ok, err, isOk, isErr
Transform: map, mapErr, flatMap, bimap, filter, tap, tapErr
Unwrap: unwrap, unwrapOr, unwrapOrElse
Combine: combine, combineAll
Convert: fromTry, fromTryWithError, fromPromise, fromPromiseWithError, toPromise, mapToOption
Async: andThen
Branch: match
Schema
Parse untrusted data into typed values. Accumulates all validation errors.
import {
validate,
object,
required,
optional,
chain,
string,
parseNumber,
min,
max,
type InferSchemaType,
} from '@railway-ts/pipelines/schema';
const userSchema = object({
name: required(string()),
age: required(chain(parseNumber(), min(18), max(120))),
email: optional(string()),
});
type User = InferSchemaType<typeof userSchema>;
// { name: string; age: number; email?: string }
const result = validate(input, userSchema);
// Result<User, ValidationError[]>Primitives: string, number, boolean, date, bigint
Parsers: parseNumber, parseInt, parseFloat, parseJSON, parseString, parseBigInt, parseBool, parseDate, parseISODate, parseURL, parseEnum
Structures: object, array, tuple, tupleOf
Unions: union, discriminatedUnion, literal
Modifiers: required, optional, nullable, emptyAsOptional
String Constraints: minLength, maxLength, pattern, nonEmpty, email, phoneNumber
Number Constraints: min, max, integer, finite, between
Enums: stringEnum, numberEnum
Combinators: chain, transform, refine, matches
Utilities: validate, formatErrors, InferSchemaType, Validator, ValidationError
Composition
Build pipelines. No nested function calls.
import { pipe, flow, curry } from '@railway-ts/pipelines/composition';
// Immediate execution
const result = pipe(
5,
(x) => x * 2,
(x) => x + 1,
); // 11
// Build reusable pipeline
const process = flow(
(x: number) => x * 2,
(x) => x + 1,
);
process(5); // 11Functions: pipe, flow, curry, uncurry, tupled, untupled
Import Patterns
Subpath imports (recommended for tree-shaking)
import { some, none, map } from '@railway-ts/pipelines/option';
import { ok, err, flatMap } from '@railway-ts/pipelines/result';
import { pipe, flow } from '@railway-ts/pipelines/composition';
import { string, number, validate } from '@railway-ts/pipelines/schema';Root imports (adds type suffixes)
import { mapOption, mapResult, pipe, ok, validate } from '@railway-ts/pipelines';Functions that exist in both Result and Option get suffixes when imported from root: mapResult, mapOption, etc. Result-only functions stay unsuffixed: mapErr, andThen.
Examples
Clone and run:
git clone https://github.com/sakobu/railway-ts-pipelines.git
cd railway-ts-pipelines
bun install
bun run examples/index.tsWhat's in there:
option/- Nullable handling patternsresult/- Error handling patternsschema/- Validation (basic, unions, tuples)composition/- Function composition techniquescomplete-pipelines/- Full examples with validation + async + logic
Start with examples/complete-pipelines/async-launch.ts for a real-world pattern.
Contributing
License
MIT © Sarkis Melkonian
