Skip to content
gitrep
Library

5d02e7barticle1 min read

Discriminated unions in practice

Model state as a set of explicit cases so the compiler tells you every place a new case needs handling.

Published

Boolean flags multiply. Two flags give four states, and usually only three of them make sense. A discriminated union names the valid states and makes the rest impossible to represent.

From flags to cases

ts
type Request<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

Each case carries only the data that exists in that state. There is no data to read while loading, so there is no way to read it by mistake.

Exhaustive handling

A switch on the discriminant narrows each branch. Assigning the leftover value to never turns a forgotten case into a compile error.

ts
function label(r: Request<string>) {
  switch (r.status) {
    case "idle": return "Ready";
    case "loading": return "Loading…";
    case "success": return r.data;
    case "error": return r.error.message;
    default: {
      const unreachable: never = r;
      return unreachable;
    }
  }
}
  1. The satisfies operatorCheck a value against a type without widening it, so you keep precise literal types.Snippet