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.

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:
TYPESCRIPTtype 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
- Add a new status
cancelledto yourTaskStatusunion type. - Observe the compiler error in your
handleTaskStatefunction. - Update the
switchstatement to include a case forcancelled. - Verify that the error disappears.
Common Pitfalls
- Returning the variable: Don't return the
_exhaustiveCheckvariable. Just assigning it is enough to trigger the compiler check. - Ignoring the Error: Never use
as neveroranyto "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, thedefaultcase will always be reachable, makingneverchecking 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
nevertype acts as a compiler-enforced safety net indefaultcases. - 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.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js — charts, tables, and real-time data that your users will actually enjoy using.


