Skip to content

Rayologist/Result-Type

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Result-Type

A fully type-safe Result<T, E> for TypeScript. Model operations that can succeed (Ok) or fail (Err) as values instead of throwing, and pattern-match on them with exhaustive, discriminant-aware type checking.

Installation

This package is source-only. Copy the files in src/ (result.ts, try-catch.ts, index.ts) into your project and import from ./result or ./index.

At a glance

import { Result, tryCatch } from "./src";

const ok = Result.Ok({ id: 1 });
const err = Result.Err(new Error("boom"));

if (ok.isOk()) {
  console.log(ok.value);
}
if (err.isErr()) {
  console.log(err.error.message);
}

value and error are readonly properties (not methods). isOk() / isErr() narrow the type, so after the guard you can reach into the correct side without casts.

Usage

1. Return Result from a use case

From examples/lib/use-case/user-login/index.ts:

import { Result } from "../../../../src";
import { InvalidCredentialsError } from "../../exception";
import { UseCase } from "../use-case.interface";

type LoginRequest = { email: string; password: string };
type LoginUser = { name: string; email: string };
type LoginResponse = Result<LoginUser, InvalidCredentialsError>;

export class LoginUseCase implements UseCase<LoginRequest, LoginResponse> {
  execute(request: LoginRequest): LoginResponse {
    const { email, password } = request;
    if (email === "admin" && password === "admin") {
      return Result.Ok({ name: "admin", email: "admin@example.com" });
    }
    return Result.Err(new InvalidCredentialsError());
  }
}

2. Branch on isOk() / isErr()

From examples/simple-login.ts:

const result = new LoginUseCase().execute(credentials);

if (result.isErr()) {
  console.log("login failed:", result.error.message);
  return;
}
console.log("logged in as:", result.value.email);

3. Combine results with Result.all

From examples/login-and-authorize.ts:

const combined = Result.all([loginResult, checkpointResult] as const);
if (combined.isErr()) {
  return combined;
}

const [user, checkpoint] = combined.value;
//    ^? [LoginUser, Checkpoint]

as const on the input tuple preserves positional types, so destructuring stays [A, B] instead of collapsing to (A | B)[]. If any input is Err, Result.all short-circuits and returns it.

4. Dispatch with Result.match

Result.match pattern-matches on the success value and on named error variants. There are two shapes:

Exhaustive — one handler per error class. Adding a new variant to the union becomes a compile error until a handler is added.

From examples/match-all-errors.ts:

Result.match(result, {
  Ok: (p) => `welcome ${p.user.name}`,
  InvalidCredentialsError: () => "wrong email or password",
  UserDisabledError: (e) => `account ${e.email} is disabled`,
  InternalServerError: () => "something went wrong, try again later",
});

With an Err fallback — handle the variants you care about, let everything else fall through. No exhaustiveness check, less boilerplate.

From examples/match-with-err-fallback.ts:

Result.match(result, {
  Ok: (p) => ({ status: 200, body: `welcome ${p.user.name}` }),
  InvalidCredentialsError: () => ({ status: 401, body: "unauthorized" }),
  Err: (e) => ({ status: 500, body: e.message }),
});

If E has no literal name discriminant (e.g. plain Error, string, unknown), the named-handler form is unavailable and an Err fallback is required.

5. Wrap throwing code with tryCatch

tryCatch turns a throwing async boundary into a Result. Use it at the edges (fetch, db, IO) so the rest of the code works in Result-land.

From examples/try-catch.ts:

const success = await tryCatch(() => fetchUser("admin"));
if (success.isOk()) {
  console.log("got user:", success.value);
}

tryCatch takes a function that returns a value or a promise (not the promise directly). That way synchronous throws inside the function are also caught, not just rejections.

An optional onFinally callback runs on both paths — see examples/try-catch-on-finally.ts for cleanup patterns.

Combine it with match to dispatch typed errors out of try/catch — see examples/try-catch-and-match.ts.

Error classes need a literal name

Named handlers in Result.match dispatch by looking up error.name at runtime and by using that name as a discriminant at the type level. For this to work, each error class must declare a literal name:

export class InvalidCredentialsError extends Error {
  readonly name = "InvalidCredentialsError";
  constructor() {
    super("Invalid credentials");
  }
}

readonly name = "InvalidCredentialsError" without as const is enough — TypeScript preserves the string literal type for readonly class fields initialized with a literal.

If you forget the name field (or set it dynamically so its type widens to string), the error variant loses its discriminant. Result.match then falls back to the Err-only form and the compiler can no longer catch missing or misspelled handlers.

API Reference

Result<T, E>

type Result<T, E = Error> = Ok<T> | Err<E>;
  • Ok<T> — success, holds a value of type T on .value.
  • Err<E> — failure, holds an error of type E on .error.
  • isOk() / isErr() — type guards that narrow to the correct variant.

Result.Ok(value) / Result.Err(error)

Factory helpers that construct Ok<T> / Err<E>.

Result.all(results)

Takes a tuple of Result objects and returns a single Result whose success value is the tuple of unwrapped values. Short-circuits on the first Err. Use as const on the input to keep the positional tuple type.

Result.match(result, handlers)

Pattern-matches on result. handlers is either:

  • { Ok, [ErrorName]: ... } — exhaustive: one handler per variant of E.
  • { Ok, Err, [ErrorName]?: ... }Err fallback for any unhandled variant; named handlers are optional.

At runtime, if no handler matches (e.g. E widened to unknown and no Err fallback was provided), match throws an Error describing which handlers were declared.

tryCatch(promise, onFinally?)

async function tryCatch<T, E = unknown>(
  fn: () => Promise<T> | T,
  onFinally?: () => Promise<void> | void,
): Promise<Result<T, E>>;

Invokes fn and returns Result.Ok(value) on success or Result.Err(error) if it throws synchronously or rejects. onFinally runs on both paths.

Contact

Questions, feedback, and PRs are always welcome. Contact me at bwchen.dev@gmail.com.

About

Fully type-safe implementation of Result type in Typescript.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors