Why is the type "never" meaningless in union types?

4.9k views Asked by At

Typescript playground

I'm having some trouble while trying to wrap my head around the fact that the type never is meaningless and can be discarded when it's inside a union type.

I mean, I get that inside an intersection type, never will immediately make everything result in never, since no type can by some type and never at the same type. It makes sense to me.

enter image description here

But in an union type, my instinct initially tells me that the never type would be a valid option. My question is why isn't it? Why does never can be discarded in a union type?

enter image description here

Could anybody give a reasonable explanation about this so I can understand it better?

2

There are 2 answers

1
jcalz On BEST ANSWER

One way to think of a type is as a set of all the values that are assignable to it. So boolean can be thought of as {true, false}, the set containing just those two values. And string can be thought of as the (essentially) infinite set containing every possible string value.

In TypeScript, never is the bottom type. It has no values. If you have a JavaScript value and you ask "is this a value of type never?" then the answer is "no". In terms of sets, never can be thought of as ∅, the empty set.

In the mapping from types to sets-of-values, the intersection operation in TypeScript (&) can be thought of as the set intersection operation (∩). If you have sets A and B, then A∩B is the set of exactly the objects which are members of both A and B. For any set A, the intersection A∩∅ with the empty set is just the empty set ∅. There are no elements in both A and the empty set, since there are no elements in the empty set at all. Back in TypeScript types, this means A & never becomes never for any type A. It would be valid if the TypeScript compiler just left string & never as string & never, but in fact it goes ahead and reduces it to never automatically, since the latter representation is simpler.

On the flip side: in the mapping from types to sets-of-values, the union operation in TypeScript (|) can be thought of as the set union operation (∪). If you have sets A and B, then A∪B is the set of exactly the objects which are members of either A or B (this is an inclusive or). For any set A, the union A∪∅ with the empty set is just A. The union contains all the elements of A and all the elements of the empty set. Since there are no elements of the empty set, that's just "all the elements of A". Back in TypeScript types, this means A | never becomes A for any type A. It would be valid if the TypeScript compiler just left string | never as string | never, but in fact it goes ahead and reduces it to string automatically, since the latter representation is simpler.

So that's the basic explanation. There are other analogies, such as boolean logic propositions like "this element is a member of this type" which is always FALSE for the never type, leading to things like A ∧ FALSE = FALSE and A ∨ FALSE = A. Or like arithmetic, where the analogy isn't exact, but intersection looks like multiplication and union looks like addition (this analogy becomes exact for pairs instead of intersection and discriminated unions instead of regular unions) and the never type is 0. But hopefully this gives enough intuition about why the compiler behaves this way.

Note that there's also a top type in TypeScript called unknown which behaves exactly as the dual to never in that A & unknown = A and A | unknown = unknown and has the dual analog in set theory (the universal set/class). But you didn't ask about that and this answer is already long enough as it is.

5
VLRoyrenn On

Explained concretely, a variable of type UNION_A = string | boolean | never can take any valid value for a boolean, any valid value for a string and any valid value for never (of which no values can exist by definition), so a union with the never type adds nothing to the domain of values this variable may end up receiving.

EDIT: The point of the never type is that it's the type of something that cannot happen, usually the return type of function that can't possibly return.

function fail() {
    throw new Error(":(");
    // What is the *return* type of that function?
}

let foo = fail(); // foo can only receive the type never
console.log(foo * 2); // Doesn't matter what foo is, this is guaranteed to be dead code

There is no possible way for fail() to return a value, not even null. It can never return, so its return type is never, and any code that operates using the return value of that function is basically just dead code.

A union with never usually means you have this

function do_thing(x) {
    let foo;
    if (x == 0) {
        fail(); // foo is of type never
    } else {
        foo = 1/x; // foo is of type number
    }

    // Foo is technically of type number | never,
    // but you can discard never since it never runs.
    let bar = foo * 2;
    return bar;
}