runtime.boot

Full Stack Systems
Production Mindset

CodeWithMihir

Engineering thoughtful products from interface to infrastructure.

CodeWithMihir

TypeScript Tutorial

TypeScript Exhaustive Checks Explained

Learn how exhaustive checks work in TypeScript using never, switch statements, discriminated unions, and safer refactoring patterns.

Welcome back! I am Mihir, and in this lesson we will learn exhaustive checks in TypeScript.

An exhaustive check makes sure you handled every possible member of a union.


The Problem

type Status = "loading" | "success" | "error";

function getMessage(status: Status) {
  switch (status) {
    case "loading":
      return "Loading...";
    case "success":
      return "Success!";
  }
}

We forgot "error".

TypeScript may not always make this obvious unless we ask it to.


Using never for Exhaustive Checks

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

never means no value should be able to reach this function.


Exhaustive switch Example

type Status = "loading" | "success" | "error";

function getMessage(status: Status) {
  switch (status) {
    case "loading":
      return "Loading...";
    case "success":
      return "Success!";
    case "error":
      return "Something went wrong";
    default:
      return assertNever(status);
  }
}

If every case is handled, status becomes never in the default block.


What Happens When You Add a New Case?

type Status = "loading" | "success" | "error" | "idle";

Now the switch is missing "idle".

The assertNever(status) line will show a TypeScript error because status is no longer never.

That is exactly what we want.


Exhaustive Checks with Discriminated Unions

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius * shape.radius;
    case "square":
      return shape.size * shape.size;
    case "rectangle":
      return shape.width * shape.height;
    default:
      return assertNever(shape);
  }
}

If a new shape is added later, TypeScript helps you update this function.


Why Exhaustive Checks Are Useful

They protect you during refactoring.

When a union grows, TypeScript points to every place that needs to handle the new option.

This is especially helpful for:

  • app states
  • payment states
  • API response states
  • UI component variants

Quick Recap

  • Exhaustive checks ensure every union member is handled.
  • The never type is used to catch missing cases.
  • assertNever is a common helper function.
  • This pattern is great with discriminated unions and switch.
  • It makes refactoring safer.

Next up, we start classes with Classes →.