ts-results
A TypeScript implementation of Rust's Result and Option types for explicit, compile-time-checked error handling.
Repository Health
Technical Analysis
ts-results ports Rust’s Result<T, E> and Option<T> types to TypeScript, turning functions that might fail or return nothing into values the type checker forces you to handle. Instead of throwing exceptions callers can silently miss, or returning T | undefined that gets forgotten downstream, functions return an Ok/Err or Some/None wrapper whose .ok/.some discriminant narrows the type at each branch.
The library is a small, dependency-free set of two classes per type (OkImpl/ErrImpl, SomeImpl/NoneImpl) exposing a fluent chain of Rust-style methods — map, mapErr, andThen, unwrapOr, expect — plus static helpers like Result.all, Result.any, and Result.wrap for combining or adapting existing throwing code. An optional ts-results/rxjs-operators subpath adds operators (resultMap, elseMap, resultSwitchMap, and others) for working with streams of Result/Option values without pulling rxjs into projects that don’t use it.
What You Get
- Result<T, E> type -
Ok/Errwrapper classes with a.ok/.errdiscriminant that TypeScript narrows automatically inifbranches. - Option<T> type -
Some/Nonewrapper (withNoneas a frozen singleton) replacingT | undefinedfor optional values. - Fluent combinators -
map,mapErr,andThen,unwrapOr,expect,expectErr, andtoOption()/toResult()conversions between the two types. - Static aggregation helpers -
Result.all/Result.anyandOption.all/Option.anyfor combining multiple Results/Options into one. Result.wrap/Result.wrapAsync- adapters that catch a throwing (sync or async) function and return aResultinstead.- Optional rxjs operators - a separate
ts-results/rxjs-operatorsentry point (resultMap,resultMapErr,elseMap,resultSwitchMap,resultMergeMap,filterResultOk,filterResultErr) for streams of Result/Option values, isolated so the core package has zero dependencies.
Common Use Cases
- Wrapping fallible file/network/parsing calls - a function that reads a file or parses input returns
Result<string, 'invalid path'>instead of throwing, forcing every call site to check.okbefore touching the value. - Replacing
T | undefinedreturn types - functions likegetLoggedInUsername()returnOption<string>so.andThen()chains replace manual null checks. - Composing multiple fallible steps -
Result.all(pizzaResult, toppingsResult)short-circuits to the first error while giving back allOkvalues as a tuple when every step succeeds. - Adapting existing throwing APIs -
Result.wrap(() => JSON.parse(input))turns a try/catch-only API into a typedResultwithout rewriting the underlying call. - Reactive pipelines over fallible streams - the rxjs operators let an
Observable<Result<T, E>>be mapped, filtered, or switched on its Ok/Err state without unwrapping manually at every step.
Under The Hood
Architecture
The library is tiny and flat — three source files (result.ts, option.ts, utils.ts) re-exported through index.ts, plus a separately packaged rxjs-operators submodule with its own package.json so it resolves as its own subpath and doesn’t force rxjs on consumers who don’t use it. Both Result and Option follow the same shape: a pair of implementation classes (OkImpl/ErrImpl, SomeImpl/NoneImpl) implementing a shared BaseResult/BaseOption interface, with the exported Ok/Err/Some bindings cast to a combined class-and-function type so they’re callable both as new Ok(x) and Ok(x). None is a single frozen singleton rather than a class instantiated per use, avoiding allocation for the empty case. There’s no DI or plugin surface — it’s a closed pair of data types with fluent instance methods and static namespace helpers (Result.all, Result.any, Result.wrap), and any change to the shared base interface would have to propagate through both Impl classes and the rxjs operator functions that pattern-match on the .ok/.some flags.
Tech Stack
Pure TypeScript with zero runtime dependencies in the core package, compiled to an ES5 target (needed for the callable-class trick used by Ok/Err/Some). Dev tooling is jest ^26 with ts-jest for tests, prettier ^2 for formatting, TypeScript ^4.2, and copyfiles to assemble the publish output — the build runs tsc twice (once for CommonJS, once with -m esnext for an ESM build into dist/esm/) and copies README.md/LICENSE/per-subpackage package.json files into dist afterward. rxjs ^6.6 is a dev dependency used only to build and test the optional operators submodule; no bundler is used, just plain tsc output.
Code Quality
Test coverage is reasonable for the surface area — result.test.ts, ok.test.ts, err.test.ts, and option.test.ts cover map/andThen/mapErr/unwrap/expect/toOption for each variant, and a separate rxjs.test.ts exercises every rxjs operator, all run under jest/ts-jest. Error handling is largely the point of the library rather than a separate concern — though unwrap()/expect() intentionally still throw when misused, and ErrImpl captures a trimmed stack trace at construction for debugging. Naming follows Rust conventions (unwrap, expect, mapErr, andThen) rather than idiomatic JS, a deliberate tradeoff for developers porting Rust patterns, and a code comment flags a known TypeScript narrowing limitation (microsoft/TypeScript#10564) that requires strictNullChecks. No CI configuration is present in the repository and no linter is configured beyond prettier for formatting.
API Design
The public API deliberately mirrors Rust’s Result/Option vocabulary (ok/err/some/none flags, unwrap, expect, map, mapErr, andThen, unwrapOr) so developers already comfortable with Rust’s error handling can adopt it with minimal relearning, and the exported Ok/Err/Some helpers work both as constructors and as plain functions to cut call-site boilerplate. The README documents nearly every method with before/after examples contrasting thrown-exception code against the Result-based equivalent, but the surface intentionally omits several Rust Result methods (contains, map_or, or_else, unwrap_or_else, and others, tracked as a comment in result.ts), so consumers expecting full Rust parity will hit gaps. Keeping the rxjs operators behind a separate subpath is a sensible ergonomic choice that keeps the core package dependency-free. Overall this is a faithful, well-executed port of an established pattern rather than a novel API design.