A TypeScript library inspired by Rust, providing Result and Option
types for safe error handling and null management with functional programming
patterns.
JavaScript/TypeScript error handling often relies on try...catch blocks or
nullable return types, which can be verbose or hide potential errors. rustify
brings Rust-inspired monads like Result and Option to TypeScript, enabling
functional programming patterns for safer code. This allows you to:
- Handle errors explicitly: Functions return a
Resultwhich is eitherOk(value)for success orErr(error)for failure. - Manage nullable values safely: Use
Optionto represent values that may or may not exist, eliminating null/undefined errors. - Improve type safety: Both
Result<T, E>andOption<T>types are tracked by the type system. - Chain operations safely: Monadic methods like
andThen,map, andorElseallow elegant functional composition. - Perform exhaustive checks: The
matchmethod ensures you handle all cases explicitly. - Easily wrap unsafe functions:
Result.fromandOption.fromNullableprovide simple ways to convert potentially unsafe operations. - Destructure results easily: Use
asTuple()for Go-style[err, val]destructuring, orasObject()if you prefer{ error, value }destructuring.
You can install rustify using your favorite package manager or directly from
jsr.
npm:
npm install @ghaerdi/rustify
# or
yarn add @ghaerdi/rustify
# or
pnpm add @ghaerdi/rustifyjsr:
npx jsr add @ghaerdi/rustify
# or
bunx jsr add @ghaerdi/rustify
# or
deno add @ghaerdi/rustifyImport Ok, Err, Result, Some, None, and Option from the library.
import { Err, None, Ok, Option, Result, Some } from "@ghaerdi/rustify";
import { match } from "@ghaerdi/rustify/match";
// --- Creating a function that returns a Result ---
function divide(
numerator: number,
denominator: number,
): Result<number, string> {
if (denominator === 0) {
return Err("Cannot divide by zero");
}
return Ok(numerator / denominator);
}
// --- Using the function and handling the Result ---
const result = divide(10, 2);
// Use match() to exhaustively handle both Ok and Err cases.
// Result.ok / Result.err match the variant and hand the UNWRAPPED
// value (or error) straight to the handler.
const message = match(result)
.with(Result.ok, (value) => `Result: ${value}`)
.with(Result.err, (error) => `Error: ${error}`)
.exhaustive(); // compile-checked: every variant handled
console.log(message); // "Result: 5"
// Working with ok() and err() methods that return Option:
const okValue = result.ok(); // Returns Option<number>
if (okValue.isSome()) {
console.log(`Ok value: ${okValue.unwrap()}`);
}
// Example with Option — Option.some / Option.none match the variants:
const name: Option<string> = Some("Alice");
const greeting = match(name)
.with(Option.some, (value) => `Hello, ${value}!`)
.with(Option.none, () => "Hello, stranger!")
.exhaustive();
console.log(greeting); // "Hello, Alice!"
// Wrapping unsafe operations
const parsed = Result.from(() => JSON.parse('{"x": 1}')); // Ok({x: 1})
const nullable = Option.fromNullable(() => document.getElementById("app")); // Some(element) or NoneResult<T, E>: Represents either success (Ok<T>) or failure (Err<E>).Ok<T>: Contains a success value. Becomes iterable ifTis iterable.Err<E>: Contains an error value.
Option<T>: Represents an optional value, eitherSome<T>orNone.Some<T>: Contains a value. Becomes iterable ifTis iterable.None(): Represents the absence of a value. CallNone()to create a None instance.
match: Type-safe pattern matching for any value — a chained.with()/.exhaustive()API inspired by ts-pattern. Matches literals, object shapes, arrays, class instances, and your own algebraic types (includingOptionandResultvia the dedicatedOption.some/Option.none/Result.ok/Result.errpatterns), with compile-time exhaustiveness checking.
Result<T, E> is a discriminated union of Ok<T> and Err<E> — narrow
with isOk() / isErr() (or the match() patterns Result.ok / Result.err,
which hand the unwrapped value or error to the handler).
- Checking:
isOk(): ReturnstrueifOk.isErr(): ReturnstrueifErr.isOkAnd(fn): ReturnstrueifOkand the value satisfiesfn.isErrAnd(fn): ReturnstrueifErrand the error satisfiesfn.contains(value): ReturnstrueifOkand the value equalsvalue.
- Extracting Values:
ok(): Returns theOkvalue asSome(value)orNone.err(): Returns theErrvalue asSome(error)orNone.unwrap(): Returns theOkvalue, throws ifErr. Use with caution.unwrapErr(): Returns theErrvalue, throws ifOk.expect(message): ReturnsOkvalue, throwsmessageifErr.expectErr(message): ReturnsErrvalue, throwsmessageifOk.unwrapOr(defaultValue): ReturnsOkvalue ordefaultValueifErr.unwrapOrElse(fn): ReturnsOkvalue or computes default usingfn(errorValue)ifErr.unwrapOrDefault(): ReturnsOkvalue or throws (noDefaulttrait in TypeScript).
- Mapping & Transformation:
map(fn): MapsOk<T>toOk<U>. LeavesErruntouched.mapErr(fn): MapsErr<E>toErr<F>. LeavesOkuntouched.mapOr(defaultValue, fn): AppliesfntoOkvalue, returnsdefaultValueifErr.mapOrElse(defaultFn, fn): AppliesfntoOkvalue, appliesdefaultFntoErrvalue.mapOrDefault(defaultValue, fn): AppliesfntoOkvalue, returnsdefaultValueifErr.
- Chaining & Side Effects:
and(res): ReturnsresifOk, else returns self (Err).andThen(fn): Callsfn(okValue)ifOk, returns the resultingResult.or(res): ReturnsresifErr, else returns self (Ok).orElse(fn): Callsfn(errValue)ifErr, returns the resultingResult.inspect(fn): Callsfn(okValue)ifOk, returns originalResult.inspectErr(fn): Callsfn(errValue)ifErr, returns originalResult.
- Flattening & Transposing:
flatten(): ConvertsResult<Result<T, E>, E>toResult<T, E>.transpose(): TransposesResult<Option<T>, E>intoOption<Result<T, E>>.
- Pattern Matching:
match(matcher): Executesmatcher.Ok(value)ormatcher.Err(error), returning the result.
- Cloning:
cloned(): Returns a newResultwith a deep clone of theOkvalue (usingstructuredClone).Errvalues are not cloned.
- Destructuring:
asTuple(): Returns[undefined, T]forOkor[E, undefined]forErr.asObject(): Returns{ error: undefined, value: T }forOkor{ error: E, value: undefined }forErr.
- Iteration:
iter(): Returns an iterator that yields theOkvalue once, or nothing ifErr.[Symbol.iterator](): Iterator protocol — yields theOkvalue if it is iterable.
- Static Methods on
Result:Result.from(fn, errorTransform?): Wraps a sync function that might throw. ReturnsOk(result)orErr(error).Result.fromAsync(fn, errorTransform?): Wraps an async function returning a Promise. ReturnsPromise<Result>.Result.isResult(value): Type guard, returnstrueifvalueisOkorErr.
Option<T> is a discriminated union — every value exposes a literal __tag:
"some" or "none" — so you can narrow with if (opt.__tag === "some") (or
isSome() / isNone()).
- Checking:
isSome(): ReturnstrueifSome.isNone(): ReturnstrueifNone.isSomeAnd(fn): ReturnstrueifSomeand the value satisfiesfn.contains(value): ReturnstrueifSomeand the value equalsvalue.
- Extracting Values:
unwrap(): Returns theSomevalue, throws ifNone. Use with caution.expect(message): Returns theSomevalue, throwsmessageifNone.unwrapOr(defaultValue): Returns theSomevalue ordefaultValueifNone.unwrapOrElse(fn): Returns theSomevalue or computes default usingfn()ifNone.unwrapOrDefault(): Returns theSomevalue or throws (noDefaulttrait in TypeScript).
- Mapping & Transformation:
map(fn): MapsSome<T>toSome<U>. LeavesNoneuntouched.mapOr(defaultValue, fn): AppliesfntoSomevalue, returnsdefaultValueifNone.mapOrElse(defaultFn, fn): AppliesfntoSomevalue, appliesdefaultFnifNone.mapOrDefault(defaultValue, fn): AppliesfntoSomevalue, returnsdefaultValueifNone.
- Chaining & Side Effects:
and(res): ReturnsresifSome, else returnsNone.andThen(fn): Callsfn(someValue)ifSome, returns the resultingOption.or(res): Returns self ifSome, else returnsres.orElse(fn): Returns self ifSome, else callsfn()and returns the result.xor(other): ReturnsSomeif exactly one of self orotherisSome, elseNone.inspect(fn): Callsfn(someValue)ifSome, returns originalOption.
- Filtering:
filter(predicate): ReturnsSome(value)ifSomeand predicate passes, elseNone.
- Flattening & Transposing:
flatten(): ConvertsOption<Option<T>>toOption<T>.transpose(): TransposesOption<Result<T, E>>intoResult<Option<T>, E>.
- Inserting & Taking (Mutating):
getOrInsert(value): Returns the contained value. IfNone, inserts and returnsvalue.getOrInsertWith(fn): Returns the contained value. IfNone, computes and insertsfn().take(): Extracts the value, leaving the option asNone. Returns the value asSome.takeIf(predicate): Extracts the value ifSomeand predicate passes, leavingNone.
- Pattern Matching:
match(matcher): Executesmatcher.Some(value)ormatcher.None(), returning the result.
- Cloning:
cloned(): Returns a newOptionwith a deep clone of theSomevalue (usingstructuredClone).
- Zipping:
zip(other): ZipsSome(a)withSome(b)intoSome([a, b]), elseNone.zipWith(other, fn): ZipsSome(a)withSome(b)usingfn(a, b)intoSome(result), elseNone.
- Iteration:
[Symbol.iterator](): Iterator protocol — yields theSomevalue if it is iterable.
- Converting to Result:
okOr(err): ConvertsSome(v)toOk(v),NonetoErr(err).okOrElse(fn): ConvertsSome(v)toOk(v),NonetoErr(fn()).
- Static Methods on
Option:Option.fromNullable(fn): Wraps a function that might returnnullorundefined. ReturnsSome(value)orNone.Option.isOption(value): Type guard, returnstrueifvalueisSomeorNone.
Import match and P from @ghaerdi/rustify/match.
- Matching:
match(value): Starts a match chain, returning aMatchyou extend with.with()cases and terminate with.exhaustive(),.otherwise()or.run().matches(value, pattern): Standalone predicate — returnstrueifvaluematchespattern.
- Terminals:
.with(pattern, handler): Adds a case.handlerreceives the value narrowed to whatpatternmatches. Returns the extended match..exhaustive(): Runs the match and throws if nothing matched. At compile time, calling it on an incomplete match is a type error at the call site that names the missing cases (e.g.NeverCase<"NonExhaustive: unhandled case { type: rect }">)..otherwise(handler): Runs the match, callinghandler(value)for anything no case matched..run(): Runs the match, returningundefinedif nothing matched — excluded from the return type when every case is covered.
Option/Resultpatterns:Option.some,Option.none,Result.okandResult.errmatch the respective variant and pass the unwrapped value (or error) to the handler —nbelow isnumber, notOption<number>:These patterns are per-variant:match(opt) .with(Option.some, (n) => n.toFixed(2)) .with(Option.none, () => "none") .exhaustive();
.with(Option.some, ...).exhaustive()alone is a compile error naming the missing variant (NeverCase<"NonExhaustive: unhandled case { __tag: none }">).- Patterns (the
Pnamespace):P.any/P._: Matches anything (catch-all).P.string,P.number,P.boolean,P.bigint,P.symbol: Matches primitive types.P.nullish: Matchesnullorundefined.P.array(pattern?): Matches arrays; optionally checks every element.P.instanceOf(Ctor): Matches class instances.P.union(...patterns): Matches any of the given patterns.P.when(guard): Matches when the type guard returnstrue.P.not(pattern): Matches everything exceptpattern.P.optional(pattern): Matchesundefinedorpattern.
- Types:
Match: the chain type returned bymatch().Pattern<TInput>: a valid pattern forTInput.Narrow<TInput, P>: the type of a value matched by patternP.
import { Err, Ok, Result } from "@ghaerdi/rustify";
function parseAge(input: string): Result<number, string> {
const num = parseInt(input, 10);
if (isNaN(num)) return Err("Not a number");
if (num < 0) return Err("Age cannot be negative");
if (num > 150) return Err("Unrealistic age");
return Ok(num);
}
const result = parseAge("25")
.map((age) => age + 1) // Ok(26)
.andThen((age) => Ok(age.toString())); // Ok("26")
console.log(result.unwrap()); // "26"import { None, Option, Some } from "@ghaerdi/rustify";
const config: Option<Record<string, string>> = Some({
theme: "dark",
lang: "en",
});
const theme = config
.map((c) => c.theme) // Some("dark")
.filter((t) => t === "dark") // Some("dark")
.unwrapOr("light"); // "dark"
console.log(theme);import { Option, Result } from "@ghaerdi/rustify";
// Result.from catches thrown errors
const parsed = Result.from(() => JSON.parse('{"valid": true}'));
// parsed is Ok({ valid: true })
const failed = Result.from(() => JSON.parse("invalid"));
// failed is Err("Unexpected token...")
// Option.fromNullable handles null/undefined
const element = Option.fromNullable(() => document.getElementById("app"));
// element is Some(element) or Noneimport { match, P } from "@ghaerdi/rustify/match";
import { Err, None, Ok, Option, Result, Some } from "@ghaerdi/rustify";
type Shape =
| { type: "circle"; radius: number }
| { type: "rect"; width: number; height: number };
// exhaustive() is checked at compile time: every Shape case must be handled.
const area = (shape: Shape): number =>
match(shape)
.with({ type: "circle" }, ({ radius }) => Math.PI * radius * radius)
.with({ type: "rect" }, ({ width, height }) => width * height)
.exhaustive();
console.log(area({ type: "circle", radius: 2 })); // ~12.57
console.log(area({ type: "rect", width: 3, height: 4 })); // 12
// Patterns can also be guards, catch-alls, and combinators:
const describe = (value: unknown): string =>
match(value)
.with(P.string, (s) => `a string: ${s}`)
.with(P.number, (n) => `a number: ${n}`)
.with(
{ type: "rect", width: P.number, height: P.number },
({ width }) => `a ${width}-wide rect`,
)
.otherwise(() => "something else");
console.log(describe("hi")); // "a string: hi"
console.log(describe({ type: "rect", width: 3, height: 4 })); // "a 3-wide rect"
// Option.some / Option.none / Result.ok / Result.err are ready-made
// patterns for the library's own types: they match the variant AND hand the
// unwrapped value (or error) straight to the handler.
const label = (value: Result<number, string> | Option<number>): string =>
match(value)
.with(Result.ok, (n) => `ok: ${n}`)
.with(Result.err, (e) => `err: ${e}`)
.with(Option.some, (n) => `some: ${n}`)
.with(Option.none, () => "none")
.exhaustive();
console.log(label(Ok(5))); // "ok: 5"
console.log(label(Err("boom"))); // "err: boom"
console.log(label(Some(5))); // "some: 5"
console.log(label(None())); // "none"This project uses Bun.
- Install Dependencies:
bun install
- Type Checking:
bun run check --watch
- Run Tests:
bun test --watch
Contributions welcome! Please submit issues and pull requests.
- Fork the repository.
- Create your feature branch.
- Commit your changes.
- Push to the branch.
- Open a Pull Request.
MIT License - see the LICENSE file for details.