Skip to main content

Overview

Result<T, E> is a discriminated union type representing the outcome of an operation that can either succeed with a value of type T or fail with an error of type E.
type Result<T, E> = Ok<T, E> | Err<T, E>

Type Structure

The Result type is a union of two variants:
  • Ok<T, E> - Represents success with a value of type T
  • Err<T, E> - Represents failure with an error of type E
Both variants use phantom types to enable proper type inference in composition:
  • Ok<T, E>: T is the actual value type, E is a phantom (unused at runtime)
  • Err<T, E>: T is a phantom (unused at runtime), E is the actual error type
This symmetric structure is essential for generator-based composition with Result.gen().

The Ok Variant

class Ok<A, E = never> {
  readonly status: "ok";
  readonly value: A;
}

Properties

status
"ok"
required
Discriminant property that identifies this as a success result
value
A
required
The success value

The Err Variant

class Err<T, E> {
  readonly status: "error";
  readonly error: E;
}

Properties

status
"error"
required
Discriminant property that identifies this as an error result
error
E
required
The error value

Type Utilities

InferOk

Extracts the success type from a Result:
type InferOk<R> = R extends Ok<infer T, unknown> ? T : never
Example:
type MyResult = Result<number, string>;
type Value = InferOk<MyResult>; // number

InferErr

Extracts the error type from a Result:
type InferErr<R> = R extends Err<unknown, infer E> ? E : never
Example:
type MyResult = Result<number, string>;
type Error = InferErr<MyResult>; // string

Discriminated Union Pattern

The Result type uses TypeScript’s discriminated union feature via the status property:
const result: Result<number, string> = getValue();

if (result.status === "ok") {
  // TypeScript narrows to Ok<number, string>
  console.log(result.value); // number
} else {
  // TypeScript narrows to Err<number, string>
  console.log(result.error); // string
}

Examples

Basic Usage

import { Result } from "better-result";

// Success case
const success: Result<number, string> = Result.ok(42);

// Error case
const failure: Result<number, string> = Result.err("Something went wrong");

With Custom Error Types

class ValidationError extends TaggedError<"ValidationError"> {
  readonly _tag = "ValidationError";
  constructor(public field: string, public message: string) {
    super();
  }
}

type ValidateResult = Result<User, ValidationError>;

function validateUser(data: unknown): ValidateResult {
  if (!data || typeof data !== "object") {
    return Result.err(new ValidationError("user", "Invalid data"));
  }
  return Result.ok(data as User);
}

Pattern Matching

const result: Result<number, string> = performOperation();

const output = result.match({
  ok: (value) => `Success: ${value}`,
  err: (error) => `Error: ${error}`,
});

Why Phantom Types?

Phantom types ensure type safety in generator-based composition:
const result = Result.gen(function* () {
  const a = yield* getUserId();    // Ok<string, DbError>
  const b = yield* getUser(a);     // Ok<User, ApiError>
  return Result.ok({ a, b });
});
// result: Result<{a: string, b: User}, DbError | ApiError>
Without phantom types, TypeScript couldn’t infer the union of all possible error types across multiple yields.

See Also