TypeScript Discriminated Unions — Pattern Matching Examples | TryJS

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

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

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.

Runnable Example

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"] }));

Open this example in the TryJS playground to edit and run the code instantly in your browser — no signup needed.

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.