Discriminated Unions and Exhaustiveness

A discriminated union gives each variant a shared property whose value is a distinct literal, often called a tag or discriminant. This is a robust way to model network states, commands, UI states, and domain events. A switch on the tag narrows each case to its corresponding object.

For exhaustive handling, use a helper that accepts never in the default branch. If a new variant is added later, the compiler can flag code that forgot to handle it. Exhaustiveness works best when the union is explicit and the discriminant values are unique.

Exhaustive state handling
type LoadState =
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

function assertNever(value: never): never {
  throw new Error(`Unexpected state: ${JSON.stringify(value)}`);
}

function render(state: LoadState): string {
  switch (state.status) {
    case "loading": return "Loading…";
    case "success": return state.data.join(", ");
    case "error": return state.message;
    default: return assertNever(state);
  }
}
đź’ˇ Each case narrows state to one variant. If a new LoadState member is added without a case, the default value is no longer never, causing a type error.
Key Takeaways
  • Use literal discriminants for stateful unions.
  • Exhaustiveness checks protect code as unions evolve.
  • Prefer explicit state variants over loosely related optional properties.
Knowledge Check

What is the purpose of `assertNever` in an exhaustive switch?