tryjs.app

TypeScript Discriminated Unions — Pattern Matching Examples

Learn TypeScript discriminated unions and exhaustive pattern matching with runnable examples. Result types, state machines, and safe switch-case.

Discriminated Unions

Exhaustive pattern matching

typescriptTypeScript Essentials
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function divide(a: number, b: number): Result<number> {
  if (b === 0) return { ok: false, error: "Division by zero" };
  return { ok: true, value: a / b };
}

function display(result: Result<number>) {
  if (result.ok) {
    console.log("Result:", result.value);
  } else {
    console.error("Error:", result.error);
  }
}

display(divide(10, 3));
display(divide(10, 0));

// State machine with exhaustiveness
type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

function render(state: State): string {
  switch (state.status) {
    case "idle":    return "Press start";
    case "loading": return "Loading...";
    case "success": return `Got ${state.data.length} items`;
    case "error":   return `Oops: ${state.message}`;
    default: {
      const _exhaustive: never = state;
      return _exhaustive;
    }
  }
}

console.log(render({ status: "loading" }));
console.log(render({ status: "success", data: ["a", "b", "c"] }));

Overview

A discriminated union (also called a tagged union) is a union type where each member has a common literal field — the 'discriminant' or 'tag' — that lets TypeScript figure out which variant you're holding. Inside a switch on that tag, TypeScript narrows to each specific variant, including its extra fields. Discriminated unions are the safest way to model 'one of N shapes' — API responses, Redux actions, state machines, Result<T> / Either types.

How It Works

Every variant gets a literal tag

type Result<T> = { ok: true; value: T } | { ok: false; error: string }. The tag here is `ok`, a literal true or false. Any distinct set of literals works: 'loading'|'success'|'error', 'add'|'remove'|'update', etc.

Narrowing happens via switch or if

if (result.ok) narrows to the ok: true variant, so result.value is accessible. else narrows to ok: false and result.error. A switch on the tag field gives you the same narrowing in each case.

Exhaustiveness with never

Assign the narrowed value to a variable of type never in the default case. If you ever add a new variant without handling it, the never assignment fails to compile — compiler-enforced exhaustiveness.

No runtime type checks needed

Because the tag is a value in the object itself, narrowing is a plain property check at runtime. No class hierarchy, no instanceof, no reflection.

Common Mistakes

  • Using an optional field as the discriminant — undefined doesn't narrow cleanly.
  • Forgetting the exhaustiveness check, then adding a new variant and getting silent undefined returns in production.
  • Making the tag a string when a boolean would be simpler (Result's ok: true/false) or vice versa.
  • Mixing several overlapping tags instead of committing to one — TypeScript can narrow, but the code becomes unreadable.

When to Use It

Any time you're modelling a fixed set of shapes where each shape carries different data: parsed AST nodes, Redux/Flux actions, API response types, loading/success/error state, Result<T, E>, network protocol messages. If you find yourself writing `data: any` because the shape varies, reach for a discriminated union instead.

Frequently Asked Questions

What is a discriminated union in TypeScript?

A union type where each member has a common literal field (the discriminant) that TypeScript uses to narrow between the variants. It's the type-safe way to model 'one of N shapes' without runtime class hierarchies.

How do I enforce exhaustive matching on a discriminated union?

Add a default case that assigns the narrowed value to a variable of type never. const _exhaustive: never = value. If you add a new variant and forget to handle it, the assignment fails to compile.

Can the discriminant be something other than a string?

Yes — literal booleans (Result's ok: true/false), numbers, and even specific object shapes work. String literals are the most common because they're readable and hard to mistype.

Are discriminated unions the same as sum types in other languages?

Conceptually, yes. Haskell's data types, Rust's enums with variants, and OCaml's variants are all sum types. Discriminated unions are TypeScript's way to express the same idea with structural typing and literal tags.