Exhaustive Switch

Two Ways to Enforce Exhaustiveness

You saw assertNever in the medium level. Another way is to declare an explicit return type and turn on noImplicitReturns, or use satisfies never. And a Record lookup table is exhaustive by construction.

Example

type Plan = "free" | "pro" | "enterprise";

function seats(plan: Plan): number {
  switch (plan) {
    case "free": return 1;
    case "pro": return 10;
    case "enterprise": return 500;
    default:
      plan satisfies never;
      throw new Error("unreachable");
  }
}

const monthlyPrice: Record<Plan, number> = { free: 0, pro: 799, enterprise: 24999 };

for (const p of ["free", "pro", "enterprise"] as const) {
  console.log(p.padEnd(10), seats(p), monthlyPrice[p]);
}

Output

free       1 0
pro        10 799
enterprise 500 24999

A New Plan Breaks the Build

Example

Adding "team" to the union produces errors in both places that must be updated (TS1360 and TS2741).

type Plan = "free" | "pro" | "team";

function seats(plan: Plan): number {
  switch (plan) {
    case "free": return 1;
    case "pro": return 10;
    default:
      plan satisfies never;
      throw new Error("unreachable");
  }
}

const monthlyPrice: Record<Plan, number> = { free: 0, pro: 799 };

Compiler output (tsc)

Line 8: error TS1360: Type '"team"' does not satisfy the expected type 'never'.
Line 13: error TS2741: Property 'team' is missing in type '{ free: number; pro: number; }' but required in type 'Record<Plan, number>'.