This TypeScript error occurs when you try to use type guards or comparisons on a value that TypeScript has determined to be unreachable (type 'never'). The fix involves proper exhaustiveness checking and understanding TypeScript's control flow analysis.
The 'never' type in TypeScript represents values that should never occur—an impossible state. When you see this error, TypeScript's control flow analysis has determined that a particular code path is unreachable, and you're attempting to perform type checks on a value that logically cannot exist. This error commonly appears when: 1. You have incomplete union type handling (missing cases) 2. TypeScript's type narrowing eliminates all possibilities before a type guard 3. You're trying to compare a 'never' value with other types using equality checks or typeof 4. Exhaustiveness checking fails due to missing branches The error message specifically mentions that you cannot test a 'never' expression against string, number, or other types because if a value can never exist, you cannot compare it to anything. TypeScript is protecting you from runtime errors by catching this logical inconsistency at compile time.
First, locate the comparison causing the error. Hover over the variable to see TypeScript's inferred type:
type Status = 'pending' | 'success' | 'error';
function handleStatus(status: Status) {
if (status === 'pending') {
console.log('Pending...');
} else if (status === 'success') {
console.log('Success!');
}
// At this point, TypeScript knows status is 'error' (not 'never' yet)
if (typeof status === 'string') { // ERROR: status is 'never' here if all cases handled
console.log('This will never run');
}
}Use your IDE's type hover feature to see what TypeScript infers at each point in your code.
Create a helper function to catch unhandled cases at compile time:
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${value}`);
}
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'triangle':
return 0.5 * shape.base * shape.height;
default:
// TypeScript knows shape is 'never' here if all cases covered
return assertNever(shape); // Compile-time error if cases missing
}
}If you add a new shape type to the union and forget to handle it, TypeScript will error at the assertNever call.
Ensure every possible value in a union is accounted for:
type Result<T> =
| { type: 'success'; data: T }
| { type: 'error'; message: string }
| { type: 'loading' };
function processResult<T>(result: Result<T>) {
if (result.type === 'success') {
console.log(result.data);
} else if (result.type === 'error') {
console.error(result.message);
} else if (result.type === 'loading') {
console.log('Loading...');
}
// All cases covered, no 'never' type
// This would cause the error if we didn't handle 'loading':
// if (typeof result.type === 'string') { ... }
}Missing any of the if blocks would leave some union members unhandled, leading to the 'never' error.
Don't perform additional type checks after all cases are handled:
// ❌ WRONG - TypeScript knows x is 'never' after the switch
function badExample(x: 'a' | 'b') {
switch (x) {
case 'a': return 1;
case 'b': return 2;
}
if (typeof x === 'string') { // ERROR: x is 'never'
console.log('Unreachable');
}
}
// ✅ CORRECT - No additional checks after exhaustive handling
function goodExample(x: 'a' | 'b') {
switch (x) {
case 'a': return 1;
case 'b': return 2;
}
// No code here - function returns in all cases
}
// ✅ ALTERNATIVE - Use default case for exhaustiveness
function betterExample(x: 'a' | 'b') {
switch (x) {
case 'a': return 1;
case 'b': return 2;
default:
const exhaustiveCheck: never = x;
return exhaustiveCheck;
}
}Define explicit type predicates for complex type guards:
interface Admin { role: 'admin'; permissions: string[] }
interface User { role: 'user'; email: string }
interface Guest { role: 'guest' }
type Person = Admin | User | Guest;
function isAdmin(person: Person): person is Admin {
return person.role === 'admin';
}
function isUser(person: Person): person is User {
return person.role === 'user';
}
function isGuest(person: Person): person is Guest {
return person.role === 'guest';
}
function getAccessLevel(person: Person) {
if (isAdmin(person)) {
return 'full';
} else if (isUser(person)) {
return 'limited';
} else if (isGuest(person)) {
return 'read-only';
}
// TypeScript knows all cases are covered
// No 'never' error when adding proper type guards
}Enable strict compiler options in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true
}
}These settings help TypeScript catch more type errors, including incomplete union handling that leads to 'never' type issues.
### Understanding TypeScript's Control Flow Analysis
TypeScript uses control flow analysis to track the possible types of variables at each point in your code. When all possible types have been eliminated through type guards, the remaining type is 'never'.
function example(x: string | number) {
if (typeof x === 'string') {
// x is string here
console.log(x.toUpperCase());
} else if (typeof x === 'number') {
// x is number here
console.log(x.toFixed(2));
} else {
// x is 'never' here - all possibilities exhausted
// TypeScript knows this code is unreachable
}
}### The 'never' Type and Type Tests
The error message specifically mentions testing against string, number, or other types because:
1. typeof value === 'string' requires value to exist
2. value === 'something' requires value to be comparable
3. instanceof checks require value to be an object
Since 'never' represents no possible values, these tests are logically impossible.
### Discriminated Unions and Exhaustiveness
For complex scenarios, use discriminated unions with a common discriminant property:
type Event =
| { type: 'click'; x: number; y: number }
| { type: 'keypress'; key: string }
| { type: 'scroll'; delta: number };
function handleEvent(event: Event) {
switch (event.type) {
case 'click':
console.log(`Clicked at (${event.x}, ${event.y})`);
break;
case 'keypress':
console.log(`Key pressed: ${event.key}`);
break;
case 'scroll':
console.log(`Scrolled by ${event.delta}`);
break;
default:
// TypeScript ensures all cases are handled
const _exhaustiveCheck: never = event;
return _exhaustiveCheck;
}
}### Edge Cases with Single-Value Unions
TypeScript has a known limitation with single-value unions:
// This might not trigger 'never' properly:
type Single = 'only';
function test(x: Single) {
if (x === 'only') {
return true;
}
// x is 'never' here, but TypeScript might not complain
}
// Workaround: use a union with never
type SingleBetter = 'only' | never;### Async Functions and Promises
'never' can appear with promise handling:
async function alwaysRejects(): Promise<never> {
throw new Error('Always fails');
}
// TypeScript knows this promise never resolves
alwaysRejects().then(
() => { /* unreachable */ },
(error) => console.error(error)
);### Type Predicates and Assertion Functions
Use type predicates for custom type guards:
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
function process(value: unknown) {
if (isStringArray(value)) {
// value is string[] here
console.log(value.join(', '));
} else {
// value is unknown here (not 'never')
console.log('Not a string array');
}
}Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript