Railway-Oriented Programming

tsentials

Result<T>, Maybe<T>, Rule Engine, DDD base classes, type-safe JSON, and functional utilities — with full async pipeline support.

npm install tsentials
npm version npm downloads bundle size tests CI TypeScript Node.js license

Railway-Oriented

Chain operations that automatically short-circuit on failure — no try/catch.

Async-first

ResultAsync<T> chains build synchronously, resolve once. Zero intermediate awaits.

Tree-shakeable

Subpath exports + sideEffects:false. Import only what you use.

Strictly typed

Built with strict:true, exactOptionalPropertyTypes, noUncheckedIndexedAccess.

DDD-ready

Entity base classes, domain events, and soft-delete via mixin factory pattern.

Zero dependencies

No runtime dependencies. Pure TypeScript that compiles to clean ESM.

Type-safe JSON

safeJsonParse and parseAndValidate return Result<T> — never throw, chain directly into any pipeline.

# Modules

tsentials/result
Result<T>, ResultAsync<T>, ResultChain<T>, fromAsync — the full railway pipeline
tsentials/maybe
Maybe<T> monad with sync + async pipeline, plus collection utilities
tsentials/errors
AppError, ErrorType enum, Err factory — structured error creation
tsentials/rules
Rule<T> function type and RuleEngine — composable validation rules
tsentials/entity
createEntityBase(), createSoftDeletable(), DomainEvent — DDD building blocks
tsentials/http
fetchResult(), RequestBuilder — type-safe HTTP calls returning Result<T>
tsentials/time
DateTimeProvider interface with real + fake implementations for testing
tsentials/clone
Cloneable<T> interface, deepClone(), cloneArray()
tsentials/union
Union<T> — discriminated union utility with exhaustive match
tsentials/json
safeJsonParse, safeJsonStringify, parseAndValidate — type-safe JSON without exceptions
tsentials/function
pipe, flow, identity, constant, flip — function composition and pipelines
tsentials/array
NonEmptyArray<T> — arrays guaranteed to have at least one element
tsentials/eq
Eq<T> — composable structural equality with struct, contramap, getArrayEq
tsentials/ord
Ord<T> — type-safe ordering with sortBy, min, max, clamp, between
tsentials/predicate
Predicate<T>, Refinement<A,B> — composable boolean conditions with and/or/not/all/any
tsentials/these
These<E,A> — partial success, a value together with errors/warnings
tsentials/tree
Tree<T> — recursive hierarchies with map, filter, fold, drawTree
tsentials/record
Functional object utilities — map, filter, pick, omit, reduce, partition

# Examples

Async pipeline — ResultAsync<T>
import { fromAsync, Err } from 'tsentials/result';

const profile = await fromAsync(fetchUser(userId))
  .andThen(user => validateUser(user))       // Result<User> | ResultAsync<User>
  .ensure(user => user.isActive, Err.validation('User.Inactive', 'Not active'))
  .map(user => user.profile)
  .tap(p => console.log('fetched', p.name))
  .match(
    profile => profile,
    () => null,
  );
Maybe<T> — null-safe pipelines
import { Maybe, tryFind, choose } from 'tsentials/maybe';

// Wrap nullable, chain operations
const display = Maybe.getOrElse(
  Maybe.filter(
    Maybe.map(Maybe.from(user.nickname), s => s.trim()),
    s => s.length > 0
  ),
  () => user.email
);

// Collection utilities
const admin = tryFind(users, u => u.role === 'admin'); // Maybe<User>
const names = choose(users.map(u => Maybe.from(u.displayName))); // string[]
Rule Engine — composable validation
import { RuleEngine } from 'tsentials/rules';
import type { Rule } from 'tsentials/rules';

const isAdult: Rule<User> = ctx =>
  ctx.age >= 18 ? Result.ok() : Result.failure(Err.validation('Age.TooLow', 'Must be 18+'));

const hasEmail: Rule<User> = ctx =>
  ctx.email ? Result.ok() : Result.failure(Err.validation('Email.Missing', 'Email required'));

const canRegister = RuleEngine.and(isAdult, hasEmail);

const result = await RuleEngine.evaluate(canRegister, user);
Type-safe JSON — never throws
import { safeJsonParse, parseAndValidate, isJsonObject } from 'tsentials/json';

// Never throws — returns Result<Json>
const result = safeJsonParse('{"name":"Alice","age":30}');
if (result.ok) {
  console.log(result.value); // { name: "Alice", age: 30 }
} else {
  console.error(result.errors[0].code); // "Json.SyntaxError" | "Json.ValidationError"
}

// Parse + type guard → fully typed Result<User>
function isUser(v: unknown): v is User {
  return isJsonObject(v) && typeof v.name === 'string' && typeof v.age === 'number';
}
const user = parseAndValidate<User>('{"name":"Alice","age":30}', isUser);
if (user.ok) console.log(user.value.name); // "Alice" — typed as User
pipe & flow — function composition
import { pipe, flow } from 'tsentials/function';

// pipe — start with a value, thread through functions
const result = pipe(
  5,
  n => n * 2,
  n => n + 1,
  n => String(n),
); // "11"

// flow — reusable composed function
const doubleAndStringify = flow(
  (n: number) => n * 2,
  n => String(n),
);
doubleAndStringify(5); // "10"
NonEmptyArray<T> — guaranteed non-empty
import { NonEmptyArray, head, asNonEmptyArray } from 'tsentials/array';

// Type-safe: the array is guaranteed to have at least one element
const items: NonEmptyArray<string> = ['a', 'b', 'c'];
head(items); // 'a' — no null check needed

// Safe conversion from plain array
const maybe = asNonEmptyArray([]);        // None
const sure  = asNonEmptyArray([1, 2]);  // Some([1, 2])
These<E,A> — partial success
import { These } from 'tsentials/these';

// A value together with warnings/errors
const parseAge = (raw: string): These<AppError, number> => {
  const age = Number(raw);
  if (Number.isNaN(age)) return These.left(Err.validation('Age.NaN', 'Not a number'));
  if (age < 0) return These.both(Err.validation('Age.Negative', 'Negative age'), 0);
  return These.right(age);
};

These.toResult(parseAge('-5')); // failure (Both → failure)
Ord<T> — type-safe ordering
import { Ord, sortBy, clamp } from 'tsentials/ord';

interface User { readonly age: number; }

const byAge = Ord.contramap(Ord.number, (u: User) => u.age);
sortBy(users, byAge); // sorted by age ascending

clamp(Ord.number, 0, 100, 150); // 100