This TypeScript error occurs when you try to access a property on a value that TypeScript has determined can never exist at runtime. It typically happens with incomplete union type handling or exhaustive checks. The fix involves proper type narrowing and handling all possible cases.
The TypeScript error "Property 'x' cannot be accessed on type 'never'" occurs when TypeScript's type system determines that a particular code path should be unreachable, yet you're trying to access properties on a value in that path. The 'never' type represents values that never occur—it's TypeScript's way of saying "this should never happen." When you see this error, TypeScript is telling you that based on its type analysis, the variable in question should have no possible values at that point in your code. This commonly happens with: 1. **Union types with unhandled cases**: When you have a union type like `A | B | C` but only handle some of the cases 2. **Exhaustive checks**: In switch statements or if/else chains where not all possibilities are covered 3. **Type narrowing**: When type guards eliminate all possible types before a certain point The error is TypeScript's way of catching logical errors in your code before runtime. If a value truly has type 'never', accessing any property on it would indeed be impossible at runtime.
First, locate where the 'never' type appears. Check what union type is involved and which cases are missing:
type Status = 'pending' | 'success' | 'error';
function handleStatus(status: Status) {
if (status === 'pending') {
console.log('Processing...');
} else if (status === 'success') {
console.log('Done!');
}
// TypeScript knows status is 'never' here - 'error' case not handled
console.log(status); // Error: Property 'length' cannot be accessed on type 'never'
}Hover over the variable in your IDE to see what type TypeScript infers. The error location shows where TypeScript thinks the code is unreachable.
Implement the standard assertNever function for exhaustive checks:
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${value}`);
}
type Status = 'pending' | 'success' | 'error';
function handleStatus(status: Status) {
switch (status) {
case 'pending':
console.log('Processing...');
break;
case 'success':
console.log('Done!');
break;
case 'error':
console.log('Error occurred');
break;
default:
assertNever(status); // Type-safe exhaustive check
}
}This pattern ensures you handle all cases. If you add a new value to the Status union and forget to handle it, TypeScript will error at the assertNever call.
For if/else chains, ensure every possible value is covered:
type Result = { type: 'data'; value: string } | { type: 'error'; message: string };
function processResult(result: Result) {
if (result.type === 'data') {
console.log(`Data: ${result.value}`);
} else if (result.type === 'error') {
console.log(`Error: ${result.message}`);
}
// All cases covered - no 'never' type
}With discriminated unions (unions with a common property like type), TypeScript can narrow the type automatically in each branch.
Define explicit type guards for complex narrowing:
interface Circle { kind: 'circle'; radius: number }
interface Square { kind: 'square'; side: number }
interface Triangle { kind: 'triangle'; base: number; height: number }
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape): number {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.kind === 'square') {
return shape.side ** 2;
} else if (shape.kind === 'triangle') {
return 0.5 * shape.base * shape.height;
}
// This should be unreachable if all cases are handled
const exhaustiveCheck: never = shape;
return exhaustiveCheck;
}The exhaustiveCheck variable forces TypeScript to verify all cases are handled.
Sometimes TypeScript converts 'never' to 'any' implicitly. Enable strict mode to catch these:
// 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
}
}Strict mode prevents implicit 'any' conversions and catches more type errors.
Check if early returns or throws affect type narrowing:
type Response = { ok: true; data: string } | { ok: false; error: string };
function handleResponse(response: Response): string {
if (!response.ok) {
throw new Error(response.error); // Early throw
}
// TypeScript knows response.ok is true here
return response.data.toUpperCase(); // No error - properly narrowed
}TypeScript tracks control flow and understands that after the throw, response.ok must be true. Make sure your control flow logic matches TypeScript's understanding.
### Understanding TypeScript's 'never' Type
The 'never' type is TypeScript's bottom type—it represents a value that never occurs. Key characteristics:
- No values are assignable to 'never'
- 'never' is assignable to every type
- Functions that never return have return type 'never'
- Type narrowing produces 'never' when all possibilities are eliminated
### Exhaustiveness Checking Patterns
1. assertNever Function: The standard pattern shown above
2. Exhaustive Switch: Using default case with type assertion
3. Satisfies Operator (TypeScript 4.9+): shape satisfies never in default case
### Common Pitfalls
1. Single-member unions: TypeScript sometimes doesn't narrow single-value unions to 'never'
2. any/unknown conversions: With strict: false, 'never' may convert to 'any' silently
3. Generic constraints: Generic types with constraints may not narrow properly
4. Mapped types: Complex mapped types can produce unexpected 'never' results
### Real-world Example: API Response Handling
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; code: number; message: string }
| { status: 'loading' };
function handleApiResponse<T>(response: ApiResponse<T>): T | null {
switch (response.status) {
case 'success':
return response.data;
case 'error':
console.error(`${response.code}: ${response.message}`);
return null;
case 'loading':
console.log('Loading...');
return null;
default:
// Type-safe exhaustive check
const _exhaustiveCheck: never = response;
return _exhaustiveCheck;
}
}### Performance Considerations
TypeScript's control flow analysis for 'never' is compile-time only—no runtime cost. The assertNever pattern adds minimal runtime overhead only when there's a bug (unhandled case).
### Testing Exhaustiveness
Write tests that verify all union cases are handled:
// Test that all Status values are handled
const statuses: Status[] = ['pending', 'success', 'error'];
statuses.forEach(status => {
expect(() => handleStatus(status)).not.toThrow();
});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