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 short-circuit on failure instead of throwing.

Async-first

ResultAsync<T> chains build synchronously and resolve with a single await at the end.

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

Pure TypeScript with no runtime dependencies, compiled to clean ESM.

Type-safe JSON

safeJsonParse and parseAndValidate return Result<T> instead of throwing, so they chain 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
tsentials/string
Case conversion — toPascalCase, toCamelCase, toKebabCase, toSnakeCase, toMacroCase, toTrainCase, toTitleCase

# Examples

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

// fromAsync takes a Promise<Result<T>> — fetchUser: (id) => Promise<Result<User>>
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';

interface User { nickname: string | null; email: string; role: string; displayName?: string; }

const users: User[] = [
  { nickname: null, email: 'trinity@zion.io', role: 'admin' },
  { nickname: '  neo  ', email: 'neo@zion.io', role: 'user', displayName: 'Neo' },
];
const user = users[1]!;

// 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';
import { Result } from 'tsentials/result';
import { Err } from 'tsentials/errors';

interface User { age: number; email: string | null; }

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>
interface User { name: string; age: number; }

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';
import { Err, type AppError } from 'tsentials/errors';

// A value together with warnings/errors — These<AppError[], A> bridges to Result
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', 'Clamped to 0')], 0);
  return These.right(age);
};

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

interface User { readonly age: number; }

const users: User[] = [{ age: 35 }, { age: 25 }, { age: 30 }];

const byAge = Ord.contramap(Ord.number, (u: User) => u.age);
sortBy(users, byAge); // [{ age: 25 }, { age: 30 }, { age: 35 }]

clamp(Ord.number, 0, 100, 150); // 100
String — case conversion
import { toPascalCase, toKebabCase, toSnakeCase, toMacroCase, toTitleCase } from 'tsentials/string';

toPascalCase('hello world');  // 'HelloWorld'
toKebabCase('HelloWorld');    // 'hello-world'
toSnakeCase('helloWorld');    // 'hello_world'
toMacroCase('helloWorld');    // 'HELLO_WORLD'
toTitleCase('helloWorld');    // 'Hello World'

// Handles spaces, hyphens, underscores, camelCase, PascalCase, and mixed input