Posted in

Understanding TypeScript’s Never Type: The Type That Never Happens

typescript

When you are working with TypeScript, you get to explicitly define the types of your variables, function arguments, and return values. This is incredibly powerful for catching errors early and making your code more predictable. While you are probably familiar with common types like string, number, boolean, Array, or even custom interface types, there is a special type that often confuses newcomers: the never type.

What is the Never Type in TypeScript?

At first glance, never might seem a bit mysterious. It is not a type you will use every day, but understanding it is key to grasping some advanced TypeScript concepts and debugging tricky type errors. Think of never as the “impossible” type. It represents values that literally never occur.

What is the Never Type?

In TypeScript, every type represents a set of possible values. For example:

  • string: Represents an infinite set of all possible text values.
  • number: Represents the set of all possible numeric values.
  • boolean: Represents the set containing just two values: true and false.

The never type, by contrast, represents an empty set of values. There is literally nothing you can assign to a variable typed as never. It is the bottom of TypeScript’s type hierarchy. If you try to assign anything to never, TypeScript will throw an error.

let myImpossibleValue: never;

// This will cause a TypeScript error:
// Type 'string' is not assignable to type 'never'.
// myImpossibleValue = "hello";

// Even 'null' and 'undefined' aren't assignable to 'never'
// myImpossibleValue = null;
// myImpossibleValue = undefined;

// The only thing assignable to 'never' is another 'never' type,
// which is usually inferred from functions that don't return.

Why Do We Need the Never Type?

You might be thinking, “Why would I ever want a type that can’t hold any value?” Good question! While you won’t declare variables of type never directly very often, it plays a crucial role in:

  1. Functions that literally never return: This includes functions that throw an error, or functions that contain an infinite loop.
  2. Exhaustive checking: Ensuring you’ve covered all possible cases in switch statements or if/else if chains.
  3. Advanced type manipulation: Especially when working with complex generic types and conditional types.
  4. Representing impossible states: When TypeScript’s control flow analysis determines a code path is unreachable.

Let’s dive into these scenarios with examples.

Never in Functions That Never Return

One of the most common places you will encounter never is as the inferred return type of functions that, well, never return control to their caller.

1. Functions That Always Throw an Error

If a function always throws an exception, it never successfully completes its execution and returns a value. TypeScript recognizes this and infers its return type as never.

function throwError(message: string): never {
    throw new Error(message);
}

// Example usage:
try {
    throwError("Something went wrong!");
} catch (error) {
    console.error(error); // This code will execute
}

// The function 'throwError' itself never "returns" a value.

2. Functions with Infinite Loops

Similarly, if a function contains an infinite loop and no break or return statements, it will never complete its execution.

function keepRunning(): never {
    while (true) {
        console.log("Still running...");
        // This loop will never end, so the function never returns
    }
}

// Calling this function would block your program indefinitely!
// keepRunning();

Never for Exhaustive Checking (The “Unreachable Code” Helper)

This is a fantastic real-world application of never that helps you write more robust code, especially when dealing with union types.

Imagine you have a function that takes a parameter which can be one of several distinct types (a union type). You want to ensure that your function handles every single possible type in that union. If you accidentally miss a case, never can help TypeScript warn you.

Let’s say we have different types of shapes:

type Circle = {
    kind: "circle";
    radius: number;
};
type Square = {
    kind: "square";
    sideLength: number;
};
type Triangle = {
    kind: "triangle";
    base: number;
    height: number;
};

type Shape = Circle | Square | Triangle; // Our union type

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.sideLength * shape.sideLength;
        // What if we forget 'triangle'?
        // We'll add a 'default' case that uses 'never'
        default:
            // This line will only be reached if 'shape' is of a type
            // that TypeScript considers impossible at this point.
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck; // TypeScript will complain here if a case is missing!
    }
}

// Now, if we later add a new shape type, like 'Triangle',
// but forget to add a 'case "triangle":' in getArea,
// TypeScript will give us an error on '_exhaustiveCheck = shape;':
// Type 'Triangle' is not assignable to type 'never'.
// This forces us to handle the new 'Triangle' case!

In this example, _exhaustiveCheck is explicitly typed as never. If we add a new Shape (like Triangle) but forget to add a corresponding case in the switch statement, the default block will be reached with shape being of type Triangle. Since Triangle is not never, TypeScript will throw a type error on const _exhaustiveCheck: never = shape;. This is a powerful way to ensure you have handled all possible inputs!

Never in Advanced Type Manipulations

The never type’s unique property of being an empty set makes it incredibly useful in advanced generic type programming, especially with conditional types and mapped types.

Filtering Union Members with Never 

Since never is automatically discarded when it’s part of a union type (e.g., string | never is just string), we can use it to “filter out” types from a union.

Let’s create a utility type that extracts all string members from a union:

type MyUnion = string | number | boolean | never; // MyUnion effectively becomes string | number | boolean

// This utility type will keep T if it extends U, otherwise it returns never.
type FilterByType<T, U> = T extends U ? T : never;

type OnlyStrings = FilterByType<MyUnion, string>; // Result: string
type OnlyNumbers = FilterByType<MyUnion, number>; // Result: number

type FilterOutUndefined<T> = T extends undefined ? never : T;
type NullableString = string | undefined;
type NonNullableString = FilterOutUndefined<NullableString>; // Result: string (undefined is filtered out)

This pattern is fundamental to many of TypeScript’s built-in utility types, like NonNullable<T>.

Removing Properties from Object Types

You can use never within mapped types to effectively remove properties from an object type. If a mapped type conditionally assigns never as the key, that key simply disappears from the resulting type.

interface UserProfile {
    id: number;
    name: string;
    email: string;
    isAdmin: boolean;
    lastLogin: Date;
}

// A utility type to create a new type that excludes properties
// whose values are assignable to a specific type.
type ExcludePropertiesByValueType<T, ExcludedType> = {
    [K in keyof T as T[K] extends ExcludedType ? never : K]: T[K];
};

// Let's create a type that excludes all boolean properties from UserProfile
type UserProfileWithoutBooleans = ExcludePropertiesByValueType<UserProfile, boolean>;

/*
UserProfileWithoutBooleans will be:
{
    id: number;
    name: string;
    email: string;
    lastLogin: Date;
}
'isAdmin' (which is boolean) has been removed!
*/

// Or exclude all string properties:
type UserProfileWithoutStrings = ExcludePropertiesByValueType<UserProfile, string>;
/*
UserProfileWithoutStrings will be:
{
    id: number;
    isAdmin: boolean;
    lastLogin: Date;
}
'name' and 'email' (which are strings) have been removed!
*/

Never in Control Flow Analysis

TypeScript’s powerful control flow analysis uses never to determine unreachable code paths. When the compiler figures out that a certain branch of your code can never be reached because all possible types have been narrowed away, the type within that branch becomes never.

function processInput(input: string | number) {
    if (typeof input === "string") {
        console.log(`Processing string: ${input.toUpperCase()}`);
    } else if (typeof input === "number") {
        console.log(`Processing number: ${input * 2}`);
    } else {
        // At this point, TypeScript has narrowed 'input' to 'never'
        // because it knows 'input' must be either a string or a number.
        // If this 'else' branch is reached, it implies an impossible state.
        const unreachable: never = input;
        // You won't be able to do anything with 'unreachable' here,
        // as its type is 'never'.
        console.log("This should never happen:", unreachable);
    }
}
// When you call processInput with string or number, the 'else' is not hit.
processInput("hello");
processInput(42);

// If for some reason 'input' could potentially be something else (e.g., 'string | number | boolean'),
// and you only checked for string and number, then 'input' in the 'else' block would be 'boolean'.
// But in this specific case, it's 'never' because string | number is exhaustive.

The Difference Between Never and Void 

This is a common point of confusion, but they are fundamentally different:


Featurenevervoid
MeaningRepresents a value that literally never occurs. The function never completes or always throws.Represents the absence of a meaningful return value. The function does complete.
Return TypeUsed for functions that don’t return (e.g., infinite loops, always throwing errors).Used for functions that return nothing explicitly (they complete, but don’t hand back a value).
AssignabilityNothing can be assigned to never (except never itself).undefined can be assigned to void. A function returning void implicitly returns undefined.
ValueHas no possible values.Represents the value undefined (or null in some contexts).
Examplefunction crash(): never { throw new Error(); }function logMessage(): void { console.log("Done"); }

In simpler terms:

  • A function that returns void finishes its job and just doesn’t give you anything specific back (it is like a polite thank-you with no gift).
  • A function that returns never never finishes its job in the normal sense; it either explodes or runs forever.

Conclusion

The never type might seem abstract at first, but it is a powerful and essential part of TypeScript’s type system. It is the type you use when something truly “never” happens or when a code path is logically impossible.

By understanding never:

  • You gain a deeper insight into how TypeScript’s control flow analysis works.
  • You can write more robust code with exhaustive checks for union types.
  • You unlock advanced techniques for type manipulation using conditional and mapped types.

While you won’t declare variables of type never on a daily basis, recognizing it in error messages and understanding its role in complex types will significantly improve your TypeScript proficiency. It is a subtle but mighty tool in your type-safe coding arsenal!

For more Details: TypeScript

Do you have any scenarios where you’ve seen never pop up and felt confused? Let’s explore them!

Leave a Reply

Your email address will not be published. Required fields are marked *