JavaScript error handling is a mess. You either throw exceptions that are invisible in type signatures, or return null | T and forget to check, or build your own { ok: boolean, data: T, error: E } shape — inconsistently, every time.
Rust has a better model. Option<T> and Result<T, E> are types that make absence and failure explicit. @rslike/std brings them to TypeScript.
npm i @rslike/std
Option<T> — values that might not exist
Instead of T | null | undefined, use Option:
import { Some, None, Option, match } from "@rslike/std";
function findUser(id: number): Option<User> {
const user = db.find(id);
return user ? Some(user) : None();
}
const result = findUser(42);
match(
result,
(user) => console.log("Found:", user.name),
() => console.log("Not found")
);
Some(value) wraps a present value. None() represents absence. The type system forces you to handle both.
Chaining operations
const name = findUser(42)
.map(u => u.name)
.unwrapOr("Anonymous");
// flatMap for operations that also return Option
const email = findUser(42)
.flatMap(u => findEmail(u.id))
.unwrapOr("*Emails are not allowed*");
Checking status
const opt = Some(123);
opt.isSome(); // true
opt.isNone(); // false
opt.unwrap(); // 123
opt.unwrapOr(0); // 123
None().unwrapOr(0); // 0
None().unwrap(); // throws UndefinedBehaviorError
Result<T, E> — operations that can fail
Instead of try/catch scattered everywhere:
import { Ok, Err, Result, match } from "@rslike/std";
function parseJSON(raw: string): Result<unknown, SyntaxError> {
return new Result((ok, err) => {
try {
ok(JSON.parse(raw));
} catch (e) {
err(e as SyntaxError);
}
});
}
const r = parseJSON('{"valid": true}');
match(
r,
(data) => console.log("Parsed:", data),
(err) => console.log("Failed:", err.message)
);
Chaining results
const config = parseJSON(rawInput)
.map(data => validate(data))
.mapErr(e => new ConfigError(e.message))
.unwrapOr(defaultConfig);
Combining with Option
import { Ok, Err, Some, None, match } from "@rslike/std";
function getPort(config: Result<Config, Error>): number {
return match(
config,
(cfg) => match(
cfg.port ? Some(cfg.port) : None(),
(p) => p,
() => 3000
),
() => 3000
);
}
match — pattern matching for Option and Result
match is a typed two-branch function that handles both states exhaustively:
import { match } from "@rslike/std";
// With Option
match(someOption, (value) => ..., () => ...);
// With Result
match(someResult, (value) => ..., (error) => ...);
// With boolean
match(flag, (t) => ..., (f) => ...);
The callback types are inferred from the input type — you can't mix up Ok and Err handlers.
Globals
For convenience, import globals once in your entry file to make Some, None, Ok, Err available without imports everywhere:
// entry.ts
import "@rslike/std/globals";
// any other file
const x = Some(42); // works without import
const r = Ok("success"); // works without import
Why not just throw?
- Thrown errors don't appear in function signatures — callers don't know what can fail
try/catch is verbose and easy to forget
null checks are easy to skip
Option and Result make the happy path and the failure path equally explicit
Install
npm i @rslike/std
Part of the rslike monorepo.