๐ŸŽ“ A1Learner
๐Ÿ”
0 of 42 lessons completed

TypeScript Exhaustive Checking

Exhaustive checking makes the compiler prove that every shape in a union is handled. A default branch that assigns the value to never compiles only when no shape reaches it.

A complete switch passes the never check

Every status has a case, so the default branch receives a value of type never. The assignment to a never constant compiles.

type DoneState = { status: "done"; value: number };
type FailedState = { status: "failed"; reason: string };
type PendingState = { status: "pending" };
type JobState = DoneState | FailedState | PendingState;
function jobLabel(state: JobState): string {
  switch (state.status) {
    case "done":
      return "done " + state.value;
    case "failed":
      return "failed " + state.reason;
    case "pending":
      return "pending";
    default: {
      const check: never = state;
      return check;
    }
  }
}
console.log(jobLabel({ status: "done", value: 3 }));
console.log(jobLabel({ status: "failed", reason: "disk" }));
console.log(jobLabel({ status: "pending" }));

The assertNever helper

assertNever accepts only a never argument, so a complete switch can call it in the default branch. The helper throws an error if a value ever reaches it at run time.

type DoneState = { status: "done"; value: number };
type FailedState = { status: "failed"; reason: string };
type PendingState = { status: "pending" };
type JobState = DoneState | FailedState | PendingState;
function assertNever(value: never): never {
  throw new Error("Unexpected state " + JSON.stringify(value));
}
function jobLabel(state: JobState): string {
  switch (state.status) {
    case "done":
      return "done " + state.value;
    case "failed":
      return "failed " + state.reason;
    case "pending":
      return "pending";
    default:
      return assertNever(state);
  }
}
console.log(jobLabel({ status: "failed", reason: "disk" }));
console.log(jobLabel({ status: "pending" }));

A missing case breaks the check

This switch drops the pending case, so the default branch receives a PendingState value. A pending state is not never, and the assignment fails.

type DoneState = { status: "done"; value: number };
type FailedState = { status: "failed"; reason: string };
type PendingState = { status: "pending" };
type JobState = DoneState | FailedState | PendingState;
function jobLabel(state: JobState): string {
  switch (state.status) {
    case "done":
      return "done " + state.value;
    case "failed":
      return "failed " + state.reason;
    default: {
      const check: never = state;
      return check;
    }
  }
}
console.log(jobLabel({ status: "done", value: 3 }));

Example

type SuccessResult = { status: "success"; data: string };
type ErrorResult = { status: "error"; message: string };
type ResultState = SuccessResult | ErrorResult;
function assertNever(value: never): never {
  throw new Error("Unexpected result " + JSON.stringify(value));
}
function renderResult(result: ResultState): string {
  switch (result.status) {
    case "success":
      return "data " + result.data;
    case "error":
      return "error " + result.message;
    default:
      return assertNever(result);
  }
}
console.log(renderResult({ status: "success", data: "report" }));
console.log(renderResult({ status: "error", message: "timeout" }));

Key points

  • The type never describes a value that cannot exist.
  • In a complete switch, the default branch narrows the value to never.
  • Assigning that default value to a never constant compiles while handling is complete.
  • Adding a shape without a case sends that shape into the default branch.
  • The never assignment then fails, and the error names the unhandled shape.
  • The assertNever helper packages the same check as a reusable function.

In this example, the switch handles both result shapes and the default branch calls assertNever, so a future third shape would fail the compile until a case is added.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท TypeScript Playground

Exercise

A new shape is added to a union and the never assignment in the default branch now fails. What does the failure mean?