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?