Skip to content
webtype.orgMake a switch exhaustive

    ↑↓ move · ⏎ open · esc close

    Patterns

    How do I get a compile error when someone adds a case to a union and forgets to handle it?

    Narrow every case and assign what is left to `never`. Once the union grows, what is left is no longer nothing, and the assignment fails.

    The recipe

    type Shape =
      | { kind: 'circle'; r: number }
      | { kind: 'square'; side: number }
    
    function assertNever(value: never): never {
      throw new Error(`Unhandled: ${JSON.stringify(value)}`)
    }
    
    function area(shape: Shape): number {
      switch (shape.kind) {
        case 'circle':
          return Math.PI * shape.r ** 2
        case 'square':
          return shape.side ** 2
        default:
          return assertNever(shape)
      }
    }

    The build compiles this and checks each result below.

    How it works

    1. 01
      function assertNever(value: never): never {

      A parameter typed `never` accepts nothing. The only value you can pass is one the compiler has already proved impossible.

    2. 02
      return assertNever(shape)

      By the default branch every known case has been returned from, so `shape` has narrowed to `never` and this compiles. Add a third shape and it stops compiling — at this line, naming the type you forgot.

    3. 03
      throw new Error

      It throws rather than returning, because a value that was supposed to be impossible arriving at run time is a bug, not a case to absorb.

    What you get

    Where it goes wrong

    It only works if every case returns or throws. A `break` that falls through to the default leaves `shape` un-narrowed, and the check quietly stops checking anything.

    Takeaway

    This is the single highest-value pattern in the language. It converts "we forgot to handle the new case" from a production incident into a compile error.

    See also

    Patterns