tuple-result is a minimal, functional, and tree-shakable Result library for TypeScript that prioritizes simplicity and serialization.
- 🔮 Simple, declarative API: Intuitive array destructuring with full type safety
- 🍃 Lightweight & Tree Shakable: Function-based design with ~150B core
- ⚡ High Performance: Minimal overhead with just a 3-element array
- 🔍 Easy Serialization: Simple array format perfect for wire transmission
- 📦 Zero Dependencies: Standalone library ensuring ease of use in various environments
- 🔧 Functional Helpers: Powerful map, unwrap, and utility functions
- 🧵 Type Safe: Full TypeScript support with literal types and type guards
📚 Examples
🌟 Motivation
Build a minimal, functional Result library that prioritizes simplicity and serialization. While libraries like ts-results and neverthrow offer robust features, their class-based implementations can create challenges with serialization and bundle size. tuple-result provides a functional alternative using simple arrays - combining minimal overhead (~150B core), easy serialization for APIs and frameworks like React Router, and helper functions while adhering to the KISS principle.
⚖️ Alternatives
📖 Usage
tuple-result provides a simple approach to error handling. Here's how to use it:
1. Creating Results
import { Err, Ok } from 'tuple-result';
const success = Ok(42);
const failure = Err('Something went wrong');
2. Working with Results
if (success.isOk()) {
console.log(success.value);
}
const [ok, error, value] = success;
if (ok) {
console.log(value);
}
const value = success.unwrap();
3. Wrapping Functions
import { t, tAsync } from 'tuple-result';
const result = t(() => JSON.parse('invalid'));
const asyncResult = await tAsync(fetch('/api/data'));
import { unwrapErr, unwrapOr } from 'tuple-result';
const value = unwrapOr(failure, 0);
5. Transforming Results
import { mapErr, mapOk } from 'tuple-result';
const doubled = mapOk(success, (x) => x * 2);
const wrapped = mapErr(failure, (e) => `Error: ${e}`);
6. Pattern Matching
import { match } from 'tuple-result';
const message = match(result, {
ok: (value) => `Success: ${value}`,
err: (error) => `Error: ${error}`
});
const processed = match(result, {
ok: (user) => ({ ...user, displayName: user.name.toUpperCase() }),
err: (error) => ({ id: 0, name: 'Unknown', error: error.message })
});
7. Serialization
const serialized = success.toArray();
const reconstructed = fromArray(serialized);
📚 API Reference
Core Functions
Ok<T, E>(value: T): OkResult<T, E>
Creates a successful result containing the given value.
const result = Ok(42);
console.log(result.unwrap());
console.log(result.isOk());
Err<T, E>(error: E): ErrResult<T, E>
Creates an error result containing the given error.
const result = Err('Something went wrong');
console.log(result.isErr());
console.log(result.error);
Type Guards
isOk<T, E>(result: TResult<T, E>): result is OkResult<T, E>
Type guard to check if a result is successful.
if (isOk(result)) {
console.log(result.unwrap());
}
isErr<T, E>(result: TResult<T, E>): result is ErrResult<T, E>
Type guard to check if a result is an error.
if (isErr(result)) {
console.log(result.error);
}
Unwrapping Functions
unwrap<T, E>(result: TResult<T, E>): T
Extracts the value from a result, throwing if it's an error.
try {
const value = unwrap(success);
} catch (error) {
}
unwrapOk<T, E>(result: TResult<T, E>): T
Extracts the value from an Ok result, throwing if it's an error.
const value = unwrapOk(success);
unwrapErr<T, E>(result: TResult<T, E>): E
Extracts the error from an Err result, throwing if it's successful.
const error = unwrapErr(failure);
unwrapOr<T, E>(result: TResult<T, E>, defaultValue: T): T
Extracts the value from a result, returning a default if it's an error.
const value = unwrapOr(failure, 0);
unwrapOrNull<T, E>(result: TResult<T, E>): T | null
Extracts the value from a result, returning null if it's an error.
const value = unwrapOrNull(failure);
Transformation Functions
mapOk<T, E, U>(result: TResult<T, E>, mapFn: (value: T) => U): TResult<U, E>
Maps the value inside an Ok result using the provided function.
const doubled = mapOk(Ok(21), (x) => x * 2);
mapErr<T, E, F>(result: TResult<T, E>, mapFn: (error: E) => F): TResult<T, F>
Maps the error inside an Err result using the provided function.
const wrapped = mapErr(Err(404), (code) => `HTTP ${code}`);
Pattern Matching Functions
match<T, E, R>(result: TResult<T, E> | TResultArray<T, E>, handlers: { ok: (value: T) => R; err: (error: E) => R }): R
Pattern matches on a result, calling the appropriate handler. Similar to Rust's match! macro.
const message = match(result, {
ok: (value) => `Success: ${value}`,
err: (error) => `Error: ${error}`
});
Function Wrappers
t<T, Args extends any[]>(fn: (...args: Args) => T, ...args: Args): TResult<T, unknown>
Wraps a synchronous function call in a Result.
const result = t(() => JSON.parse('invalid'));
const safeDivide = (a: number, b: number) => t(() => a / b, a, b);
tAsync<T>(promise: Promise<T>): Promise<TResult<T, unknown>>
Wraps a Promise in a Result.
const result = await tAsync(fetch('/api/data'));
Serialization Functions
toArray() (Instance Method)
Converts a result to a plain array for serialization.
const result = Ok(42);
const serialized = result.toArray();
fromArray<T, E>(array: TResultArray<T, E>): TResult<T, E>
Creates a result instance from a plain array.
const result = fromArray([true, undefined, 42]);
❓ FAQ
Why both TResult and TResultArray?
TResultArray is a subset of TResult - same array structure, but TResult adds convenience methods.
TResult: Full-featured classes with .isOk(), .unwrap(), .value methods
TResultArray: Plain arrays perfect for serialization (React Router, APIs, JSON)
Key benefit: All helper functions work with both types seamlessly.
const classResult = Ok('hello');
const arrayResult = [true, undefined, 'hello'] as const;
isOk(classResult);
isOk(arrayResult);
When do I use each?
Use TResult by default. You get TResultArray from:
- React Router loaders:
useLoaderData()
- JSON parsing:
JSON.parse(response)
- API responses
For serialization: result.toArray() → send over network → use helpers directly on received arrays or deserialize using fromArray(result).
No conversion needed - helpers work with both!
Why do helper functions have overloads?
TypeScript compatibility. Since TResult (classes) and TResultArray (plain arrays) have the same structure but different types, overloads ensure all helper functions work seamlessly with both:
unwrapOr(Ok(42), 0);
unwrapOr([true, undefined, 42], 0);
unwrapOr(someResult, 0);
Without overloads, complex types can cause TypeScript errors:
const result: TResultArray<User, Error> = [false, new Error('Not found'), undefined];
unwrapOr(result, defaultUser);
Overloads ensure compatibility in all scenarios.
💡 Resources / References