Dead simple Result pattern for TypeScript.
No .map(), no .flatMap(), no .andThen(), no .orElse(), no .unwrap(), no monadic gymnastics. Just a type, two functions, and TypeScript doing what TypeScript already does well.
There are dozens of Result libraries for TypeScript. Nearly all of them bolt on method chaining, transformation pipelines, and functional programming utilities that turn a simple concept into an entire paradigm.
This package does one thing: gives you a type-safe Result<T, E> discriminated union with ok() and err() constructors. You use if/else to handle it. TypeScript narrows the type for you. That's the whole API.
The entire implementation is ~35 lines. Zero runtime dependencies. Four exports.
If you need .map().flatMap().andThen().orElse().unwrapOr() chains, use neverthrow or ts-results. They're good libraries. This isn't that.
pnpm add @mkvlrn/result| Export | What it does |
|---|---|
Result<T, E> |
Union type representing success or failure |
AsyncResult<T, E> |
Promise<Result<T, E>> for async operations |
ok(value) |
Creates a success result |
err(error) |
Creates an error result |
That's it. That's the whole thing.
import { type Result, type AsyncResult, ok, err } from "@mkvlrn/result";function divide(a: number, b: number): Result<number, Error> {
if (b === 0) {
return err(new Error("Division by zero"));
}
return ok(a / b);
}
const result = divide(10, 2);
if (result.isOk) {
console.log(result.value); // number - TypeScript knows
}
if (result.isError) {
console.log(result.error.message); // Error - TypeScript knows
}No .unwrap(). No .expect(). Just an if statement and the compiler handles the rest.
async function fetchUser(id: number): AsyncResult<User, Error> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return err(new Error(`HTTP ${response.status}`));
}
const user = await response.json();
return ok(user);
} catch (error) {
return err(error instanceof Error ? error : new Error("Unknown error"));
}
}AsyncResult<T, E> is just Promise<Result<T, E>>. It's a type alias, not a class, not a wrapper, not a monad.
class ValidationError extends Error {
readonly code: number;
constructor(code: number, message: string) {
super(message);
this.name = "ValidationError";
this.code = code;
}
}
function validateEmail(email: string): Result<string, ValidationError> {
if (!email.includes("@")) {
return err(new ValidationError(400, "bad-email"));
}
return ok(email);
}
const result = validateEmail("invalid-email");
if (result.isError) {
// TypeScript knows this is a ValidationError, not just Error
console.log(`${result.error.code}: ${result.error.message}`); // 400: bad-email
}MIT