Articles
TypeScript28 Oct 2025

Types that make invalid states unrepresentable

A practical tour of modelling a domain so the compiler catches the bugs you'd otherwise ship.

Types that make invalid states unrepresentable

The best bug is the one you cannot write down. When a type makes an invalid state unrepresentable, the compiler stops being a spell-checker and starts being a proof assistant — every illegal combination becomes a syntax error long before it becomes a support ticket.

The boolean trap

Consider a request modelled with a fistful of flags:

interface RequestState {
  isLoading: boolean;
  isError: boolean;
  data: User | null;
  error: Error | null;
}

Four fields, sixteen combinations — and only four of them are real. What does isLoading: true with a non-null error even mean? The type permits it, so eventually some code path produces it, and now you are debugging a state that should never have existed.

Make the illegal unspellable

A discriminated union collapses those sixteen ghosts down to the four states that actually occur:

type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: Error };

Now data exists only in the success branch and error only in the error branch. There is no syntax for the contradiction. The bug class is gone — not caught, not handled, gone.

This is the whole move, applied over and over: push correctness into the type so the machine enforces it for free, and spend your attention on the problems that are genuinely hard.