How to fix An expression of type 'never' cannot be tested against a string, number, or other type in TypeScript
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.
Quick fix
Fixes An expression of type 'never' cannot be tested against a string, number, or other 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');
}
}- Identify where TypeScript infers 'never'
- Add exhaustiveness checking with assertNever
- Handle all union cases explicitly
- Avoid type tests after exhaustive handling
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 handledconsole.log('This will never run');}}An expression of type 'never' cannot be tested against a string, number, or other type
What this error means
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.
Typical symptoms
- TypeScript compiler error: 'An expression of type 'never' cannot be tested against a string, number, or other type'
- Error appears when using typeof checks on union types
- Occurs with equality comparisons (===, !==) in switch statements or if/else chains
- Related to discriminated union type guards
- Appears after TypeScript has narrowed a type to 'never'
- Common in exhaustive switch statements with missing default cases
Common causes
- Missing cases in union type handling (most common)
- Incomplete type guards that don't cover all union members
- Using typeof or instanceof on values that TypeScript knows are unreachable
- Missing default case in switch statements with union types
- Type narrowing that eliminates all possibilities before a comparison
- Incorrect type predicate functions that don't handle all cases
- Using equality checks on values that have been narrowed to 'never'
How to fix "An expression of type 'never' cannot be tested against a string, number, or other type"
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.
Advanced notes
### 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');
}
}Frequently asked questions
- What does "An expression of type 'never' cannot be tested against a string, numbe" mean?
- 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.
- How do I fix "An expression of type 'never' cannot be tested against a string, numbe"?
- 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.
- What causes "An expression of type 'never' cannot be tested against a string, numbe"?
- Missing cases in union type handling (most common) Incomplete type guards that don't cover all union members Using typeof or instanceof on values that TypeScript knows are unreachable
- Is the "An expression of type 'never' cannot be tested against a string, numbe" error serious?
- It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.