Exhaustiveness Checking in Switch Statements Using TypeScript's never Type

Learn how to use TypeScript's never type in switch statement default clauses to ensure compile-time exhaustiveness checking for union types.

When using switch statements with Union Types in TypeScript, it is important to ensure that all cases are exhaustively covered. If a new type is added to the union type but the switch statement is not updated, it can lead to unexpected bugs.

A technique for performing this exhaustiveness check at compile time is to assign the never type in the default clause of the switch statement.

What Is the never Type?

The never type is a special type in TypeScript that represents “a value that never occurs.” It is used for the type of unreachable code paths, such as when a function never returns (enters an infinite loop, always throws an exception, etc.).

More details: TypeScript Deep Dive: never

In simple terms, the never type represents something that is “impossible.”

How Exhaustiveness Checking with never Works

Here is a sample implementation of this technique:

type Sample = "A" | "B"; // Define a union type

function testFunc(sample: Sample): void {
  switch (sample) {
    case "A":
      console.log("Case A:", sample);
      break;
    case "B":
      console.log("Case B:", sample);
      break;
    default:
      // Exhaustiveness check here
      // If 'sample' is not of type 'never', a compile error occurs
      const _: never = sample;
      // If there is any possibility of reaching here, TypeScript reports an error
  }
}

// Usage examples
testFunc("A"); // "Case A: A"
testFunc("B"); // "Case B: B"
// testFunc("C"); // Compile error: Argument of type '"C"' is not assignable to parameter of type '"A" | "B"'.

Explanation

  1. type Sample = "A" | "B"; defines a union type Sample that only allows the string literals "A" or "B".
  2. The switch (sample) statement branches processing based on the value of sample.
  3. In the default clause, after both case "A" and case "B" have been handled, the type of sample should theoretically be in an “impossible” state. If sample is a value that is neither "A" nor "B", it would reach this default clause.
  4. The line const _: never = sample; is the key. TypeScript’s type checker expects the type of sample to be never at this point.
    • If the case clauses in the switch statement cover all patterns of the union type, there is no value of sample that can reach the default clause, so sample is inferred as type never, and compilation succeeds.
    • For example, if | "C" is added to the union type Sample but case "C": is forgotten, the type of sample reaching the default clause is inferred as "C", not never. Attempting to assign a value of type "C" to a variable of type never causes TypeScript to report a compile error.

By using this technique, you can guarantee at compile time that all patterns of a Union type have been handled. This ensures that when a new type is added to the Union type in the future, the absence of corresponding handling in the switch statement is detected early as an error.

References