Type Never
TypeScript void
vs never
- Complete Guide
The Difference Between void
and never
Return Types
The difference between void
and never
return types in TypeScript relates to whether the function is expected to complete execution or not.
void
Return Type
A function that returns void
completes execution normally but doesn't return a meaningful value. The function runs, does its work, and then ends.
function logMessage(msg: string): void {
console.log(msg);
// Function completes normally, just doesn't return anything useful
}
function updateDatabase(data: any): void {
// Do some database work
database.update(data);
// Function finishes successfully
}
never
Return Type
A function that returns never
never completes normal execution. It either throws an exception, enters an infinite loop, or otherwise never reaches its end.
function throwError(message: string): never {
throw new Error(message);
// This line is never reached
}
function infiniteLoop(): never {
while (true) {
// This loop never ends
}
}
function exhaustiveCheck(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
Key Differences
- Execution completion:
void
functions finish running;never
functions don't - Reachability: Code after calling a
never
function is unreachable - Type checking:
never
is useful for exhaustiveness checking in switch statements or conditional logic - Assignment: You can assign
never
to any type, but you can't assign anything tonever
Practical Example
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value.toString();
} else {
// TypeScript knows this should never happen
return exhaustiveCheck(value); // value is type 'never' here
}
}
The never
type helps TypeScript catch logical errors and ensures your code handles all possible cases.
Most Common Use Cases for the never
Type
1. Exhaustive Checking in Switch Statements
This is probably the most common use case - ensuring all cases are handled:
type Status = 'pending' | 'completed' | 'failed';
function handleStatus(status: Status): string {
switch (status) {
case 'pending':
return 'Processing...';
case 'completed':
return 'Done!';
case 'failed':
return 'Error occurred';
default:
// If you add a new status later and forget to handle it,
// TypeScript will catch it here
const exhaustiveCheck: never = status;
throw new Error(`Unhandled status: ${exhaustiveCheck}`);
}
}
2. Functions That Always Throw Errors
Error handling functions that never return normally:
function assertNever(message: string): never {
throw new Error(message);
}
function validateAge(age: number): number {
if (age < 0) {
assertNever('Age cannot be negative');
}
return age;
}
3. Removing Properties from Union Types
Using never
to filter out unwanted types:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result = NonNullable<string | number | null>; // string | number
// Or excluding specific types
type ExcludeString<T> = T extends string ? never : T;
type NoStrings = ExcludeString<string | number | boolean>; // number | boolean
4. Unreachable Code Marking
Indicating code paths that should theoretically never execute:
function processUserInput(input: string): string {
const trimmed = input.trim();
if (trimmed.length === 0) {
throw new Error('Empty input');
}
if (trimmed.length > 1000) {
throw new Error('Input too long');
}
// This should never happen based on our logic above
if (trimmed.length < 0) {
const unreachable: never = trimmed;
throw new Error(`Impossible state: ${unreachable}`);
}
return trimmed;
}
5. Generic Constraints and Conditional Types
Creating more precise type definitions:
// Only allow non-empty arrays
type NonEmptyArray<T> = T extends readonly [] ? never : T;
// Ensure a function parameter is never undefined
type RequiredParam<T> = T extends undefined ? never : T;
function processArray<T extends NonEmptyArray<any[]>>(arr: T): T[0] {
return arr[0]; // Safe because we know array isn't empty
}
6. State Machine Type Safety
Ensuring invalid state transitions are caught:
type State = 'idle' | 'loading' | 'success' | 'error';
type ValidTransitions = {
idle: 'loading';
loading: 'success' | 'error';
success: 'idle';
error: 'idle';
};
function transition<From extends State, To extends State>(
from: From,
to: To extends ValidTransitions[From] ? To : never
): To {
return to;
}
// This works
transition('idle', 'loading');
// This causes a TypeScript error
// transition('idle', 'success'); // Error!
7. API Response Type Guards
Ensuring all response types are handled:
type APIResponse =
| { type: 'success'; data: any }
| { type: 'error'; message: string }
| { type: 'loading' };
function handleResponse(response: APIResponse): string {
switch (response.type) {
case 'success':
return `Got data: ${response.data}`;
case 'error':
return `Error: ${response.message}`;
case 'loading':
return 'Loading...';
default:
// Catches if new response types are added
const unhandled: never = response;
throw new Error(`Unhandled response: ${unhandled}`);
}
}
8. Preventing Invalid Function Calls
Creating functions that can't be called with certain arguments:
function createUser<T>(
data: T extends { id: any } ? never : T
): User {
// Prevents creating users with existing IDs
return { id: generateId(), ...data };
}
// This works
createUser({ name: 'John', email: 'john@example.com' });
// This causes an error
// createUser({ id: '123', name: 'John' }); // Error!
Summary
The never
type is particularly powerful for catching design-time errors and ensuring your code handles all possible cases, making your applications more robust and maintainable. Use it whenever you need to:
- Ensure exhaustive case handling
- Mark unreachable code paths
- Create type-safe APIs that prevent invalid usage
- Build robust error handling systems
- Implement complex type constraints in generic code