Result<T>, Maybe<T>, Rule Engine, DDD base classes, type-safe JSON, and functional utilities — with full async pipeline support.
Chain operations that automatically short-circuit on failure — no try/catch.
ResultAsync<T> chains build synchronously, resolve once. Zero intermediate awaits.
Subpath exports + sideEffects:false. Import only what you use.
Built with strict:true, exactOptionalPropertyTypes, noUncheckedIndexedAccess.
Entity base classes, domain events, and soft-delete via mixin factory pattern.
No runtime dependencies. Pure TypeScript that compiles to clean ESM.
safeJsonParse and parseAndValidate return Result<T> — never throw, chain directly into any pipeline.
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,
);
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[]
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);
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
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"
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])
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)
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