Result<T>, Maybe<T>, Rule Engine, DDD base classes, type-safe JSON, and functional utilities, with full async pipeline support.
Chain operations that short-circuit on failure instead of throwing.
ResultAsync<T> chains build synchronously and resolve with a single await at the end.
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.
Pure TypeScript with no runtime dependencies, compiled to clean ESM.
safeJsonParse and parseAndValidate return Result<T> instead of throwing, so they chain into any pipeline.
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,
);
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[]
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);
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
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';
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
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
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