@justscale/result
v0.1.2
Published
Lightweight, type-safe Result and Option implementation for TypeScript
Downloads
274
Maintainers
Readme
@justscale/result
A lightweight, type-safe Result and Option implementation for TypeScript.
Features
- Plain branded objects -
ok()anderr()return lightweight, JSON-serializable objects - Phantom type brands - Compile-time safety without runtime overhead
- Both fail-fast and accumulating collect - with compile-time type distinction (
Accumulated<E>) - Generator/stream support -
ResultStream<T, E>for async iteration - Zero dependencies - Just TypeScript
Installation
npm install @justscale/result
# or
pnpm add @justscale/result
# or
yarn add @justscale/resultQuick Start
import { ok, err, isOk, Result } from "@justscale/result";
// Create results
const success = ok(42);
const failure = err("something went wrong");
// Type guard narrowing
if (isOk(success)) {
console.log(success.value); // 42
}
if (!isOk(failure)) {
console.log(failure.error); // "something went wrong"
}
// Get value with default
const value = Result.okOr(failure, 0); // 0API Reference
Result Types
type Ok<T> = { readonly ok: true; readonly value: T };
type Err<E> = { readonly ok: false; readonly error: E };
type Result<T, E> = Ok<T> | Err<E>;Constructors
ok<T>(value: T): Ok<T>
Creates a successful result containing value.
const result = ok(42);
// { ok: true, value: 42 }err<E>(error: E): Err<E>
Creates a failed result containing error.
const result = err("not found");
// { ok: false, error: "not found" }Type Guards
isOk<T, E>(result: Result<T, E>): result is Ok<T>
Returns true if the result is Ok, narrowing the type.
const result: Result<number, string> = ok(42);
if (isOk(result)) {
// TypeScript knows result.value exists
console.log(result.value);
} else {
// TypeScript knows result.error exists
console.log(result.error);
}Note: There is no
isErrfunction. Use!isOk(result)instead.
Result Namespace
Static utility functions for working with Results.
Result.okOr<T, E, U>(result: Result<T, E>, defaultValue: U): T | U
Get the value from a Result, or return a default if Err.
const success = ok(42);
const failure = err("error");
Result.okOr(success, 0); // 42
Result.okOr(failure, 0); // 0Result.collect<T, E>(results: Result<T, E>[]): Result<T[], E>
Collect an array of Results into a Result of array. Fail-fast: returns the first error encountered.
const results = [ok(1), ok(2), ok(3)];
const collected = Result.collect(results);
// { ok: true, value: [1, 2, 3] }
const withError = [ok(1), err("oops"), ok(3)];
const failed = Result.collect(withError);
// { ok: false, error: "oops" }Result.collectAll<T, E>(results: Result<T, E>[]): Result<T[], Accumulated<E>>
Collect an array of Results, accumulating all errors. Returns Accumulated<E> which is type-distinct from E[].
const results = [ok(1), err("a"), ok(2), err("b")];
const collected = Result.collectAll(results);
// { ok: false, error: ["a", "b"] }Result.partition<T, E>(results: Result<T, E>[]): { ok: T[]; err: E[] }
Partition results into successes and failures. Never fails.
const results = [ok(1), err("a"), ok(2), err("b")];
const { ok: successes, err: failures } = Result.partition(results);
// successes: [1, 2]
// failures: ["a", "b"]Result.fromTry<T>(fn: () => T): Result<T, Error>
Wrap a synchronous function that might throw.
const result = Result.fromTry(() => JSON.parse(input));Result.fromAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>>
Wrap an async function that might throw.
const result = await Result.fromAsync(() => fetch("/api/data").then((r) => r.json()));Stream Utilities
For working with async generators of Results.
ResultStream<T, E>
type ResultStream<T, E> = AsyncGenerator<Result<T, E>, void, unknown>;collectStream<T, E>(stream: ResultStream<T, E>): Promise<Result<T[], E>>
Collect all successful values until the first error.
collectStreamAll<T, E>(stream: ResultStream<T, E>): Promise<Result<T[], Accumulated<E>>>
Collect all results, accumulating errors.
takeUntilErr<T, E>(stream: ResultStream<T, E>): AsyncGenerator<T, Result<void, E>, unknown>
Iterate yielding values until an error is encountered.
async function* fetchPages(): ResultStream<Page, Error> {
for (let i = 1; i <= 10; i++) {
const result = await Result.fromAsync(() => fetchPage(i));
yield result;
}
}
// Collect all or fail on first error
const allPages = await collectStream(fetchPages());
// Or iterate with early termination
for await (const page of takeUntilErr(fetchPages())) {
process.stdout.write(`Processing page ${page.id}...`);
}Option Type
For representing optional values without null or undefined.
Option Types
type Some<T> = { readonly some: true; readonly value: T };
type None = { readonly some: false };
type Option<T> = Some<T> | None;Constructors
some<T>(value: T): Some<T>
const option = some(42);
// { some: true, value: 42 }none<T = never>(): Option<T>
const option = none();
// { some: false }Note:
none()returns a singleton instance for memory efficiency.
Type Guards
isSome<T>(option: Option<T>): option is Some<T>
const option: Option<number> = some(42);
if (isSome(option)) {
console.log(option.value); // 42
}Note: There is no
isNonefunction. Use!isSome(option)instead.
Option Namespace
Static utility functions for working with Options.
Option.someOr<T, U>(option: Option<T>, defaultValue: U): T | U
Get the value from an Option, or return a default if None.
const present = some(42);
const absent = none<number>();
Option.someOr(present, 0); // 42
Option.someOr(absent, 0); // 0Option.toResult<T, E>(option: Option<T>, error: E): Result<T, E>
Convert an Option to a Result.
const token: Option<string> = getAuthToken();
const result = Option.toResult(token, "Missing token");
// Ok<string> if Some, Err<"Missing token"> if NoneDesign Philosophy
Plain Objects Over Classes
Results and Options are plain objects with type brands, not class instances. This means:
- JSON serializable - Send over the wire, store in databases
- No prototype chain - Minimal memory footprint
- Structural typing - Works with any object matching the shape
// These are equivalent
const a = ok(42);
const b = { ok: true, value: 42 } as Ok<number>;
JSON.stringify(a) === JSON.stringify(b); // trueType-Level Error Accumulation
Accumulated<E> is branded at the type level to distinguish accumulated errors from single errors:
type Accumulated<E> = E[] & { readonly __accumulated: true };
// These have different types
const single: Result<User[], ValidationError> = Result.collect(results);
const accumulated: Result<User[], Accumulated<ValidationError>> = Result.collectAll(results);Examples
API Request with Error Handling
import { ok, err, isOk, Result } from "@justscale/result";
interface User {
id: string;
name: string;
}
interface ApiError {
code: number;
message: string;
}
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return err({ code: response.status, message: response.statusText });
}
return ok(await response.json());
} catch (e) {
return err({ code: 0, message: e instanceof Error ? e.message : "Unknown error" });
}
}
// Usage
const result = await fetchUser("123");
if (isOk(result)) {
console.log(`Hello, ${result.value.name}!`);
} else {
console.error(`Error ${result.error.code}: ${result.error.message}`);
}Form Validation
import { ok, err, isOk, Result } from "@justscale/result";
interface FormData {
email: string;
password: string;
age: number;
}
function validateEmail(email: string): Result<string, string> {
return email.includes("@") ? ok(email) : err("Invalid email format");
}
function validatePassword(password: string): Result<string, string> {
return password.length >= 8 ? ok(password) : err("Password must be at least 8 characters");
}
function validateAge(age: number): Result<number, string> {
return age >= 18 ? ok(age) : err("Must be 18 or older");
}
function validateForm(data: FormData): Result<FormData, string[]> {
const results = [
validateEmail(data.email),
validatePassword(data.password),
validateAge(data.age),
];
const collected = Result.collectAll(results);
if (!isOk(collected)) {
return err(collected.error as string[]);
}
return ok(data);
}Optional Configuration
import { some, none, isSome, Option } from "@justscale/result";
interface Config {
apiKey?: string;
timeout?: number;
retries?: number;
}
function getConfig(): Option<Config> {
const raw = localStorage.getItem("config");
if (!raw) return none();
try {
return some(JSON.parse(raw));
} catch {
return none();
}
}
const config = getConfig();
const timeout = isSome(config) && config.value.timeout !== undefined
? config.value.timeout
: 5000;Requirements
- Node.js >= 20.0.0
- TypeScript >= 5.0 (for best type inference)
License
MIT
