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

  1. Execution completion: void functions finish running; never functions don't
  2. Reachability: Code after calling a never function is unreachable
  3. Type checking: never is useful for exhaustiveness checking in switch statements or conditional logic
  4. Assignment: You can assign never to any type, but you can't assign anything to never

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: