Back to Blog
Lesson 39 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 26, 20263 min read

Exhaustive Switch Statements: A Guide to TypeScript Safety

Learn to use the never type to ensure your switch statements are exhaustive. Prevent logic gaps and catch bugs at compile-time in your TypeScript applications.

TypeScriptExhaustiveSwitchNeverType SafetyBest Practices
A close-up of a stop button on a public bus, highlighting travel and safety features.

Previously in this course, we explored enforcing strict null checks to ensure our variables are always accounted for. In this lesson, we take that safety mindset a step further: we’re going to ensure our switch statements are "exhaustive," meaning they account for every possible value of a type.

The Problem: Silent Logic Gaps

When we use a switch statement against a union of types, we often forget to update our logic when that union grows. Imagine we have a TaskStatus type. If we add a new status like Archived later, the compiler won't complain if our existing switch statement ignores it. This leads to silent bugs where your UI simply does nothing for new states.

We can solve this by leveraging the never type. The never type represents values that should never occur. By assigning the result of a default case to a variable typed as never, we force the TypeScript compiler to check if any unhandled cases remain.

Implementing Exhaustive Checks

To make a switch statement exhaustive, we use the default block as a catch-all for values that haven't been handled. If the logic is truly exhaustive, the code inside default should never be reached.

Here is how we apply this to our task client:

TYPESCRIPT
type TaskStatus = CE9178">'todo' | CE9178">'in-progress' | CE9178">'done';

function getStatusLabel(status: TaskStatus): string {
  switch (status) {
    case CE9178">'todo':
      return CE9178">'To Do';
    case CE9178">'in-progress':
      return CE9178">'In Progress';
    case CE9178">'done':
      return CE9178">'Completed';
    default:
      // This line forces the compiler to check exhaustiveness
      const _exhaustiveCheck: never = status;
      return _exhaustiveCheck;
  }
}

If you add 'archived' to TaskStatus but forget to add a case 'archived': to the switch, TypeScript will throw an error at const _exhaustiveCheck: never = status;. It will tell you that type 'archived' is not assignable to type never. This is exactly what we want: the compiler stops you before you deploy a broken state.

Worked Example: Advancing the Task API Client

In our building the task api client, we defined statuses for our tasks. Let's ensure our task processing function is future-proof.

TYPESCRIPT
// types.ts
export type TaskStatus = CE9178">'todo' | CE9178">'in-progress' | CE9178">'done' | CE9178">'blocked';

// client.ts
import { TaskStatus } from CE9178">'./types';

export function handleTaskState(status: TaskStatus) {
  switch (status) {
    case CE9178">'todo':
      console.log(CE9178">'Task is pending.');
      break;
    case CE9178">'in-progress':
      console.log(CE9178">'Task is active.');
      break;
    case CE9178">'done':
      console.log(CE9178">'Task is finished.');
      break;
    case CE9178">'blocked':
      console.log(CE9178">'Task is waiting on dependencies.');
      break;
    default:
      const _exhaustiveCheck: never = status;
      return _exhaustiveCheck;
  }
}

By adding blocked to our TaskStatus, we had to update the switch. If we had forgotten, the compiler would have highlighted the default case as the point of failure.

Hands-on Exercise

  1. Add a new status cancelled to your TaskStatus union type.
  2. Observe the compiler error in your handleTaskState function.
  3. Update the switch statement to include a case for cancelled.
  4. Verify that the error disappears.

Common Pitfalls

  • Returning the variable: Don't return the _exhaustiveCheck variable. Just assigning it is enough to trigger the compiler check.
  • Ignoring the Error: Never use as never or any to "silence" the compiler error. If the compiler tells you a case is missing, it is right.
  • Incomplete Unions: This pattern only works well with union types. If you use a broad type like string, the default case will always be reachable, making never checking impossible.

FAQ

Why use never instead of just throwing an error? Throwing an error happens at runtime. By assigning to never, you catch the missing case at compile-time, which is much safer and easier to debug.

Does this work with Discriminated Unions? Yes, this is actually the primary use case for mastering discriminated unions. The compiler uses the discriminant property to narrow the type down to never automatically.

Recap

  • Exhaustive checking ensures your code handles all possible states defined in a union.
  • The never type acts as a compiler-enforced safety net in default cases.
  • This pattern turns potential runtime logic bugs into compile-time errors.

Up next: We will look at Mapped Types to create dynamic transformations of our data objects.

Similar Posts