Syncing account progress…

Back

Intermediate · 14 min

Exhaustive state checks

Use never to make an omitted union case a compile-time error.

Handle every alternative

After every possible union member has been handled, the remaining value has type never. Assigning it to a never variable makes a newly added, unhandled case visible to the compiler. Do not cast a value to never to silence this check.

type Mode = "read" | "write";
function label(mode: Mode): string {
  switch (mode) {
    case "read": return "Reading";
    case "write": return "Writing";
    default: { const missing: never = mode; return missing; }
  }
}
console.log(label("write"));

Output

Writing

Keep a runtime boundary

An exhaustive switch checks values already described by a union. It does not validate external data. A throwing helper is useful for unexpected runtime values, but input still needs validation before you treat it as a trusted union.

function unreachable(value: never): never {
  throw new Error("Unexpected state: " + String(value));
}
type State = { kind: "idle" } | { kind: "done"; count: number };
function count(state: State): number {
  switch (state.kind) {
    case "idle": return 0;
    case "done": return state.count;
    default: return unreachable(state);
  }
}
console.log(count({ kind: "done", count: 3 }));

Output

3

Put it into practice

  1. Read the types and predict the output before running the examples.
  2. Fix the task without using "any" or a type assertion, then check both practice questions.

Try it yourself

Code runs on this device. When you are signed in, drafts sync to your account.

Handle the added state

Add the missing "paused" case and return "Paused". Keep the never check so future omissions remain visible.

Show solution
type State = "idle" | "active" | "paused";
function label(state: State): string {
  switch (state) {
    case "idle": return "Ready";
    case "active": return "Working";
    case "paused": return "Paused";
    default: { const missing: never = state; return missing; }
  }
}
const result = label("paused");
console.log(result);

Adding the missing branch leaves no possible State in the default branch. The never assignment now compiles and continues to detect future omissions.

Practice