verdict-ts
v1.0.0
Published
Rust-inspired Result<T, E> for TypeScript — make fallible operations type-safe
Maintainers
Readme
verdict-ts
Make fallible operations type-safe — Rust-inspired Result<T, E> for TypeScript
zero dependencies · 491 bytes gzipped · JSON-serializable · works everywhere
Why verdict-ts?
try/catch makes errors invisible in type signatures. You don't know a function can fail until it throws at runtime. verdict-ts makes success and failure both explicit in the type system — the compiler enforces that you handle errors.
// Before: invisible failure
function parseJSON(input: string): unknown { // can throw? who knows!
return JSON.parse(input);
}
// After: explicit failure
function parseJSON(input: string): Result<unknown> {
return trySync(() => JSON.parse(input));
}No classes, no prototypes, no dependencies. Just plain objects: { ok: true, value } or { ok: false, error }. They serialize to JSON, survive structuredClone, and work in every runtime.
verdict-ts vs result.ts
| | verdict-ts | result.ts |
|---|---|---|
| Bundle size | 491B gzipped | ~3KB+ gzipped |
| Dependencies | Zero | Zero |
| Implementation | Plain objects, no classes | Class-based |
| JSON serialization | ✅ JSON.stringify(result) works | ❌ Class instances don't serialize |
| structuredClone | ✅ Works | ❌ Class instances fail |
| Method chaining | ✅ .map().flatMap().unwrapOr() | ✅ |
| Standalone functions | ✅ map(r, fn) for tree-shaking | ❌ Methods only |
| Error type default | Result<T> → Result<T, Error> | Result<T, unknown> |
| Tuple combine | ✅ combine([ok(1), ok("x")]) preserves types | ❌ Arrays only |
| Async support | tryAsync() built-in | Separate ResultAsync class |
| Runtime overhead | Near zero (plain objects + closures) | Class instantiation per Result |
When to pick verdict-ts: You want the smallest possible Result type that works everywhere — browsers, Edge, Deno, Bun — and serializes to JSON.
When to pick result.ts: You want a full-featured Result toolkit with operators like andThen, orElse, asyncMap, and a richer API surface.
✨ Features
- Discriminated union —
result.oknarrows the type, noinstanceofneeded - Method chaining —
result.map(fn).flatMap(fn2).unwrapOr(default) - Standalone functions —
map(result, fn)for tree-shaking - Async support —
tryAsync()wraps Promises safely - Combine results —
combine([r1, r2, r3])validates in bulk with tuple preservation - Zero dependencies — nothing in
node_modulesexcept dev tools - 491 bytes gzipped — smaller than this README
- Universal — Node.js, Bun, Deno, Cloudflare Workers, browsers
🚀 Quick Start
import { ok, err, trySync, match } from "verdict-ts";
// Create results directly
const user = ok({ name: "Alice", age: 30 });
const failure = err(new Error("not found"));
// Wrap throwable functions
const parsed = trySync(() => JSON.parse('{"key": "value"}'));
// Pattern match to handle both cases
const message = match(parsed, {
ok: (data) => `Got: ${JSON.stringify(data)}`,
err: (error) => `Failed: ${error.message}`,
});📦 Installation
# npm
npm install verdict-ts
# pnpm
pnpm add verdict-ts
# yarn
yarn add verdict-ts
# bun
bun add verdict-tsDeno via npm specifier:
import { ok, err } from "npm:verdict-ts";📖 Usage
Creating Results
import { ok, err, trySync, tryAsync } from "verdict-ts";
// Direct construction
const success = ok(42); // Ok<number>
const failure = err(new Error()); // Err<Error>
// With custom error types
type ValidationError = { field: string; message: string };
const invalid = err<ValidationError>({ field: "email", message: "required" });
// Wrap synchronous throwable code
const config = trySync(() => JSON.parse(readFileSync("config.json", "utf-8")));
// Wrap async throwable code
const response = await tryAsync(() => fetch("https://api.example.com/data"));Transforming Results
import { ok } from "verdict-ts";
const result = ok("hello");
// map: transform the success value
const upper = result.map(s => s.toUpperCase());
// Ok<"HELLO">
// flatMap: chain operations that return Results
const length = result.flatMap(s =>
s.length > 0 ? ok(s.length) : err(new Error("empty string"))
);
// Ok<5>
// mapErr: transform the error (on Err, passes through on Ok)Extracting Values
import { ok, err, match } from "verdict-ts";
const result = ok(42);
// unwrap: returns value or throws
const value = result.unwrap(); // 42
// unwrapOr: returns value or fallback
const safe = err(new Error("fail")).unwrapOr(0); // 0
// match: pattern matching with full type narrowing
const label = match(result, {
ok: (value) => `Success: ${value}`,
err: (error) => `Error: ${error.message}`,
});Combining Results
Validate multiple fields at once:
import { ok, err, combine } from "verdict-ts";
type FieldError = { field: string; reason: string };
function validateName(name: string) {
return name.trim().length > 0
? ok(name.trim())
: err({ field: "name", reason: "required" });
}
function validateAge(age: number) {
return age >= 0 && age <= 150
? ok(age)
: err({ field: "age", reason: "out of range" });
}
const fields = combine([
validateName("Alice"),
validateAge(30),
]);
// Ok<["Alice", 30]> — tuple types preserved
const badFields = combine([
validateName(""),
validateAge(999),
]);
// Err<{ field: "name"; reason: "required" }> (first error)Type Guards
import { isOk, isErr } from "verdict-ts";
const result = someOperation();
if (isOk(result)) {
console.log(result.value); // TypeScript knows: value exists
} else {
console.log(result.error); // TypeScript knows: error exists
}Standalone Functions (Tree-Shaking)
Every method is also exported as a standalone function for optimal bundle sizes:
// Method chaining (convenient)
result.map(fn).flatMap(fn2).unwrapOr(defaultValue);
// Standalone functions (tree-shakeable)
import { map, flatMap, unwrapOr } from "verdict-ts";
unwrapOr(flatMap(map(result, fn), fn2), defaultValue);Both styles are type-safe and produce identical results. Bundlers can eliminate unused standalone functions.
⚙️ API Reference
Types
| Type | Description |
|------|-------------|
| Ok<T> | Success variant with value: T and ok: true |
| Err<E> | Failure variant with error: E and ok: false |
| Result<T, E = Error> | Union Ok<T> \| Err<E> |
| AsyncResult<T, E = Error> | Alias for Promise<Result<T, E>> |
Constructors
| Function | Signature | Description |
|----------|-----------|-------------|
| ok | (value: T) => Ok<T> | Create a success result |
| err | (error: E) => Err<E> | Create a failure result |
| trySync | (fn: () => T) => Result<T> | Wrap a sync function, catch throws |
| tryAsync | (fn: () => Promise<T>) => AsyncResult<T> | Wrap an async function, catch rejections |
Methods (on Ok / Err objects and as standalone)
| Method | Standalone | Description |
|--------|-----------|-------------|
| .map(fn) | map(result, fn) | Transform Ok value, pass Err through |
| .mapErr(fn) | mapErr(result, fn) | Transform Err error, pass Ok through |
| .flatMap(fn) | flatMap(result, fn) | Chain a function returning Result, flatten |
| .unwrap() | unwrap(result) | Return value or throw on Err |
| .unwrapOr(def) | unwrapOr(result, def) | Return value or fallback |
| .match(cases) | match(result, cases) | Pattern match with { ok, err } callbacks |
Utilities
| Function | Signature | Description |
|----------|-----------|-------------|
| combine | (results: Result<T, E>[]) => Result<T[], E> | All Ok → Ok of array, any Err → first Err |
| isOk | (result) => result is Ok<T> | Type guard for Ok |
| isErr | (result) => result is Err<E> | Type guard for Err |
🏗️ Architecture
Pure objects, no classes. Every Result is a frozen-shape plain object with ok: true | false as the discriminant. Methods are bound via closures at construction time and delegate to standalone functions — so bundlers can tree-shake anything you don't use.
Circular import resolved. methods.ts imports constructors (ok, err) and constructors.ts imports methods (map, flatMap, ...). ES module hoisting handles this cleanly at runtime.
Error default. Result<T> defaults E to Error, so simple cases don't need explicit error typing. Specialize when you need domain-specific errors.
🧪 Running Tests
npm test # run all tests
npm run typecheck # type-check without emit
npm run build # build ESM + CJS + declarations