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 betrue
Demo<number | number>
will befalse
Demo<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:
type Eq<T, U> = T extends U ? (U extends T ? true : false) : false;
const x: Eq<"a" | never, "a"> = true;
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.