TL;DR: The Quick Fix
You've likely hit this while trying to force exhaustive checking in a switch or if/else block. The compiler is complaining because it thinks a value can still leak through to your default case.
To fix it, verify two things:
- Narrow the input: Your variable must be a specific union (like
'success' | 'error'), not a genericstring. - Close the gaps: Every possible value in that union must be handled by a
casebefore it hits theneverassignment.
// The pattern for success
type Status = 'active' | 'inactive';
function handleStatus(status: Status) {
switch (status) {
case 'active':
return 1;
case 'inactive':
return 0;
default:
// This only compiles if all 'Status' variants are handled above
const _exhaustiveCheck: never = status;
return _exhaustiveCheck;
}
}
The Scenario
Imagine you're refactoring a billing service. You decide to be a proactive engineer and implement an exhaustive check. This pattern acts as a tripwire; if a teammate adds a new payment method next month, the compiler will break until they handle the new logic. It’s a great safety net.
But then you see it. Instead of a helpful nudge, the compiler throws a cryptic error:
Type 'string' is not assignable to type 'never'. (TS2322)
It feels counterintuitive. You’ve written the cases, yet TypeScript insists the variable is still a generic string and refuses to let you assign it to never.
Why Is This Happening?
In TypeScript, never is the "impossible" type. By assigning a variable to never in a default block, you are making a bold claim: "It is logically impossible for code to ever reach this line."
The TS2322 error pops up when TypeScript finds a hole in your logic. Here are the three most common culprits:
1. Your Input is Too Broad
TypeScript is cautious. If your function argument is typed as a generic string rather than a specific union of literals, the compiler assumes any text could enter the function. Even if you handle 'A' and 'B', the input could be 'Z'. Since 'Z' would fall into the default block, the type there is string, not never.
2. The "Missing Case" Trap
This often happens after an update. Suppose you expanded a union from 2 variants to 3 (e.g., adding 'pending' to 'success' | 'failed') but didn't update your switch statement. TypeScript sees that 'pending' is still unhandled. It tries to pass that value into your default block, triggering the error.
3. Type Widening
Complex logic or improper casting can sometimes cause a specific literal type to "widen" back into a string. Once that specificity is lost, the compiler can no longer guarantee the default block is unreachable.
How to Fix It
Approach 1: Tighten the Input Type
Start by checking where the data comes from. If it’s from an API response or a form input, it might be loosely typed. Use a Type Guard or a specific union definition to narrow it down.
// ❌ Problem: 'status' is just a string
function process(status: string) {
switch (status) {
case 'open': return;
default:
const _ex: never = status; // Error: string is not never
}
}
// ✅ Solution: Use a strict union
type TicketStatus = 'open' | 'closed';
function process(status: TicketStatus) {
switch (status) {
case 'open': return;
case 'closed': return;
default:
const _ex: never = status; // No error!
}
}
Approach 2: Use an 'assertNever' Helper
Manual assignments are fine, but a utility function is cleaner. It provides a clear runtime error if someone bypasses the compiler using as any. It also makes your intent obvious to other developers.
function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
type Plan = 'basic' | 'pro' | 'enterprise';
function getPrice(plan: Plan) {
switch (plan) {
case 'basic': return 10;
case 'pro': return 20;
case 'enterprise': return 50;
default:
return assertNever(plan);
// If you add a 'premium' plan later, this line will immediately flag TS2345
}
}
Approach 3: Watch Out for Enums
When working with Enums, ensure you are switching on the Enum type itself. Comparing an Enum to a raw string can break the compiler's ability to narrow the type correctly.
enum UserRole { Admin, Editor }
function checkAccess(role: UserRole) {
switch (role) {
case UserRole.Admin: return true;
case UserRole.Editor: return true;
default:
const _check: never = role; // Works as expected
}
}
Verification Steps
- Inspect the variable: Hover over the variable inside your
defaultblock in VS Code. If the tooltip says(parameter) status: "pending", you simply forgot to handle that specific case. If it saysstatus: string, your input type is too loose. - Search for casts: Look for
as stringin your codebase. Over-casting often strips away the union types that the exhaustive check relies on. - Test the gap: Add a temporary
casefor the value TypeScript is complaining about. If the error vanishes, you've confirmed a missing branch in your logic.
Summary
The TS2322 error isn't a bug in the compiler; it's the compiler doing exactly what you asked it to do. It found a hole in your logic where a value could slip through. Fix it by narrowing your inputs or by explicitly handling every possible variant in your union.

