Distributing Conditional Types Over never
In TypeScript, I wanted to write a generic type that had optional type params. The behaviour of the type would change depending on whether the optional type is provided. Here is a contrived example that I had initially written to test this:
type Foo<T = never> = T extends never ? true : false;
const x: Foo;
const y: Foo<string>;
Surprisingly, x in this example resolves to the never type instead of
true. I realized this behaviour is particular to never since if you have a
conditional with non-bottom type like so:
T extends string ? true : false
...things would behave as I would expect. I realized at this point, my
understanding of the never type is incomplete and I found
this GitHub issue in the
TS project to help me understand. This conditional type expression using
extends is called a
distributive conditional type
when run on a union. Consider the following:
type Demo<T> = T extends string ? true : false;
If T is a union type, this ternary is distributed across all union members.
type T = U | V
// The extends check will be applied to each part of the union.
U extends string ? true : false | V extends string ? true : false
This means:
Demo<string | string>will betrueDemo<number | number>will befalseDemo<string | number>will beboolean
Now consider when T is the never type. never is the empty union (a union
with no members). This can be demonstrated by:
const x: string = "" as string | never;
Since T has no members, extends is never applied , so T extends never
would never be applied in our first example, thus the result is never. I think
I was naively thinking about extends never like you would a equality check but
I find it helpful to think of types more as categories/sets than values.
So how do you make the type optional? Well it turns out you can do something
like this and the Foo type will be a boolean as expected.
type Foo<T = never> = [T] extends [never] ? true : false;
This seems to work because [T] is guaranteed to be a non-empty union, so the
extends [never] will always distribute over it. It's a bit hard to rationalize
what [never] represents though. Is [never] the same as []? TS seems to
disagree:
cont x: [never] = []; // This fails
Interestingly, the following works:
const x: never[] = [];
My intuition is that:
never[]is an array of nevers.[]is a valid instance of this.[never]is a 1-tuple with a value that can never occur. So[never]can never occur. So I don't think a runtime representation of[never]exists.