@iahuang/result-ts
v2.0.1
Published
A Typescript implementation of Rust's [`std::result::Result`](https://doc.rust-lang.org/std/result/enum.Result.html) enum type. Make errors in fallible subroutines predictable and impossible to ignore.
Readme
result-ts
A TypeScript implementation of Rust's std::result::Result enum type for type-safe error handling. Make errors in fallible functions predictable and impossible to ignore.
This library provides a lightweight, type-safe way to handle errors without exceptions. It aims to:
- Stay close to Rust's original API design
- Provide strict type safety with full TypeScript inference
- Use plain objects as opposed to classes to allow for JSON serialization
- Support both synchronous and asynchronous operations
Installation
npm install @iahuang/result-tsQuick Start
Define your error types, then use resultType() to create a type-safe result builder:
import { resultType, chain, match } from '@iahuang/result-ts';
type ParseErrors = {
invalid_number: string;
negative: number;
};
const parseResult = resultType<number, ParseErrors>();
function parsePositiveInt(s: string): Result<number, ParseErrors> {
const n = Number.parseInt(s, 10);
if (Number.isNaN(n)) return parseResult.err("invalid_number", s);
if (n < 0) return parseResult.err("negative", n);
return parseResult.ok(n);
}Chain operations and handle results:
const value = chain(parsePositiveInt("42"))
.map((n) => n * 2)
.unwrapOr(0);
// Or use pattern matching for full control
match(parsePositiveInt("-5"), {
ok: (n) => console.log(`Got ${n}`),
err: (e) => console.log(`Error: ${e.type}`)
});Core Concepts
Result Types
A Result<T, E> is either:
- Success, containing a value of type
T{ ok: true, value: T } - Failure, containing an
Err<E>under theerrorfield{ ok: false, error: Err<E> }
Where Err<E> is a discriminated union of { type: K, detail: E[K] } for each key K in E.
This is analogous to creating a Result<T, E> type in Rust, with E being an enum type with variants for different error types. The constraint that E be an enum-like type is baked into this library's type system to encourage the implementation of fixed, statically-known sets of error types which can each be handled appropriately.
Creating Results
You can create results in two ways:
1. Recommended: Using resultType()
This method provides the strongest type safety and inference.
type MyErrors = {
not_found: string; // error detail is a string (e.g., file path)
invalid_input: number; // error detail is a number (e.g., line number)
timeout: undefined; // error with no detail
};
const myResult = resultType<string, MyErrors>();
const success = myResult.ok("data");
const error1 = myResult.err("not_found", "/path/to/file");
const error2 = myResult.err("invalid_input", 42);
const error3 = myResult.err("timeout"); // no detail needed for undefined2. Using ok() and err() directly
For simple cases:
import { ok, err } from '@iahuang/result-ts';
const success = ok(42);
const failure = err("error_code", "error detail");Working with Results
Pattern Matching
Use the match function for clean error handling:
import { match } from '@iahuang/result-ts';
const message = match(parsePositiveInt("123"), {
ok: (value) => `Success: ${value}`,
err: (e) => `Error: ${e.type} - ${e.detail}`
});For exhaustive matching on individual error variants, use matchNested:
import { matchNested } from '@iahuang/result-ts';
const message = matchNested(parsePositiveInt("123"), {
ok: (value) => `Success: ${value}`,
err: {
invalid_number: (s) => `Invalid number: ${s}`,
negative: (n) => `Negative number: ${n}`,
}
});Method Chaining API
For readability, use the chain() function for a fluent API:
import { chain } from '@iahuang/result-ts';
const value = chain(parsePositiveInt("123"))
.map((n) => n * 2)
.map((n) => `Result: ${n}`)
.unwrapOr("Failed");
console.log(value); // "Result: 246"This closely resembles Rust's method chaining:
let value = parse_positive_int("123")
.map(|n| n * 2)
.map(|n| format!("Result: {}", n))
.unwrap_or("Failed".to_string());Common Operations
Transforming values with map
import { resultMap } from '@iahuang/result-ts';
const result = parsePositiveInt("42");
const doubled = resultMap(result, (n) => n * 2);
// Ok(84)Providing defaults with unwrapOr
import { resultUnwrapOr } from '@iahuang/result-ts';
const value = resultUnwrapOr(parsePositiveInt("invalid"), 0);
// Returns 0 since parsing failedChaining operations with andThen
import { resultAndThen } from '@iahuang/result-ts';
function divide(a: number, b: number): Result<number, { divide_by_zero: undefined }> {
if (b === 0) return divResult.err("divide_by_zero");
return divResult.ok(a / b);
}
const result = resultAndThen(
parsePositiveInt("10"),
(n) => divide(n, 2)
);
// Ok(5)Error handling with inspect
import { resultInspect, resultInspectErr } from '@iahuang/result-ts';
const result = parsePositiveInt("123");
// Inspect success values (for logging, debugging, etc.)
resultInspect(result, (value) => {
console.log("Parsed value:", value);
});
// Inspect errors
resultInspectErr(result, (err) => {
console.error("Parse error:", err);
});Async Support
All Result functions have async versions for working with Promises:
import { asyncResultMap, asyncResultAndThen, resultFromThrowingAsyncFunction } from '@iahuang/result-ts';
async function fetchUser(id: string): Promise<Result<User, FetchErrors>> {
return resultFromThrowingAsyncFunction(
async () => {
const response = await fetch(`/api/users/${id}`);
return await response.json();
},
(e) => fetchResult.err("network_error", String(e))
);
}
// Chain async operations
const userEmail = await asyncResultMap(
fetchUser("123"),
(user) => user.email
);
// Or use asyncChain for a fluent API
import { asyncChain } from '@iahuang/result-ts';
const email = await asyncChain(fetchUser("123"))
.map((user) => user.email)
.unwrapOr("[email protected]");Exception Handling
Convert throwing code to Results:
import { resultFromThrowingFunction, resultType, Result } from '@iahuang/result-ts';
type JsonErrors = {
parse_error: string;
};
const jsonResult = resultType<any, JsonErrors>();
function parseJSON(jsonString: string): Result<any, JsonErrors> {
return resultFromThrowingFunction(
() => JSON.parse(jsonString),
(e) => jsonResult.err("parse_error", String(e))
);
}
const data = parseJSON('{"valid": "json"}');
// Ok({ valid: "json" })
const invalid = parseJSON('invalid json');
// Err({ ok: false, error: { type: "parse_error", detail: "SyntaxError: ..." } })Complete Example: Config Loader
A realistic example showing how to chain fallible operations:
import { resultType, chain, match } from '@iahuang/result-ts';
import * as fs from 'fs';
type ConfigErrors = { not_found: string; invalid_json: string };
const configResult = resultType<any, ConfigErrors>();
function loadConfig(path: string) {
// Read file
let content: string;
try {
content = fs.readFileSync(path, 'utf-8');
} catch {
return configResult.err("not_found", path);
}
// Parse JSON
try {
return configResult.ok(JSON.parse(content));
} catch (e) {
return configResult.err("invalid_json", String(e));
}
}
// Usage
const dbHost = chain(loadConfig("config.json"))
.map((config) => config.database.host)
.unwrapOr("localhost");API Reference
Core Types
Result<T, E>- The main Result typeErr<E>- Error type (discriminated union of{ type, detail })resultType<T, E>()- Creates type-safe Result constructors
Query Methods
resultIsOk(result)- Returns true if OkresultIsErr(result)- Returns true if ErrresultIsOkAnd(result, fn)- Returns true if Ok and predicate matchesresultIsErrAnd(result, fn)- Returns true if Err and predicate matches
Transformation Methods
resultMap(result, fn)- Transform the Ok valueresultMapErr(result, fn)- Transform the Err valueresultMapOr(result, default, fn)- Transform or return defaultresultMapOrElse(result, defaultFn, fn)- Transform or compute default
Extraction Methods
resultUnwrap(result)- Extract Ok value or throwresultExpect(result, msg)- Extract Ok value or throw with messageresultUnwrapOr(result, default)- Extract Ok value or return defaultresultUnwrapOrElse(result, fn)- Extract Ok value or compute defaultresultOk(result)- Convert toT | nullresultErr(result)- Convert toErr<E> | null
Combinators
resultAnd(result, other)- Returns other if OkresultAndThen(result, fn)- Chains Ok valuesresultOr(result, other)- Returns other if ErrresultOrElse(result, fn)- Computes alternative if ErrresultFlatten(result)- Flattens nested Results
Utilities
These utility functions do not have any analogous functions in Rust, but are provided for convenience.
match(result, handlers)- Pattern matchingmatchNested(result, handlers)- Pattern matching with per-variant error handlerschain(result)- Method chaining APIresultFromThrowingFunction(fn, errFn)- Convert exceptions to ResultsresultFromThrowingAsyncFunction(fn, errFn)- Async exception handlingresultFromThrowingPromise(promise, errFn)- Promise rejection handlingresultAll(results)- Combine multiple Results into one
All sync functions have async equivalents prefixed with async:
asyncResultMap,asyncResultAndThen,asyncChain,asyncResultAll, etc.
Comparison with Other Libraries
vs. neverthrow
Unlike neverthrow, this library uses plain objects instead of class instances:
- Results can be safely serialized/deserialized (e.g., JSON.stringify)
- Closer to functional programming principles
vs. Effect
Effect provides a much more comprehensive and opinionated ecosystem implementing functional programming principles in TypeScript. This library is focused on providing a lightweight, type-safe framework for error handling without any additional tooling, dependencies, or specific use cases in mind.
Migrating from v1
Breaking Changes
Result structure changed
Errors are now nested under an error field instead of being spread flat on the result object. The error object uses type instead of error for the variant key.
// v1
{ ok: false, error: "not_found", detail: "/path" }
// v2
{ ok: false, error: { type: "not_found", detail: "/path" } }This means code that accessed result.error or result.detail directly on a failed result must now access result.error.type and result.error.detail.
Ok<T> and Undetailed<E> types removed
Ok<T> is no longer exported as a separate type. Use Result<T, never> if you need to represent a result that is always successful. Undetailed<E> has been removed entirely.
ResultType<T> helper type removed
The ResultType<typeof myResult> pattern for extracting a Result type from constructors is no longer supported. Use Result<T, E> directly instead.
// v1
const parseResult = resultType<number, ParseErrors>();
type ParseResult = ResultType<typeof parseResult>;
// v2
const parseResult = resultType<number, ParseErrors>();
type ParseResult = Result<number, ParseErrors>;resultType() no longer returns chain constructors
The okChain and errChain constructors have been removed from the object returned by resultType(). Use chain() to wrap results instead.
// v1
const r = myResult.okChain(value);
// v2
const r = chain(myResult.ok(value));tryCatchResult functions renamed
| v1 | v2 |
|----|-----|
| tryCatchResult | resultFromThrowingFunction |
| tryCatchResultAsync | resultFromThrowingAsyncFunction |
| tryCatchResultPromise | resultFromThrowingPromise |
asyncMatch replaced by matchNested
asyncMatch has been removed. The new matchNested function provides per-variant error handlers instead.
// v1
await asyncMatch(asyncResult, {
ok: async (value) => ...,
err: (e) => ...
});
// v2
matchNested(result, {
ok: (value) => ...,
err: {
not_found: (detail) => ...,
timeout: (detail) => ...,
}
});match error handler receives Err<E> instead of the full result
The err handler in match now receives the Err<E> value (i.e. { type, detail }) rather than the entire result object.
// v1
match(result, {
ok: (v) => ...,
err: (e) => console.log(e.error, e.detail)
});
// v2
match(result, {
ok: (v) => ...,
err: (e) => console.log(e.type, e.detail)
});