If you have a conditional type where the type being checked (to the left of extends
) is a generic type parameter, it becomes a distributive conditional type. So in
type GetTestDist<T = never> = T extends never ? string : number;
type GetTestNonDist<T = never> = [T] extends [never] ? string : number;
GetTestDist<T>
is a distributive conditional type because T
is a generic type parameter, while GetTestNonDist<T>
is not because [T]
is not a generic type parameter.
Yes, [T]
involves the generic type parameter T
, but it is not a type parameter itself. Sometimes people will say that the T
in T extends never ? ⋯ : ⋯
is a bare type parameter or a naked type parameter, as opposed to the T
in [T] extends [never] ? ⋯ : ⋯
which is, let's say, "clothed".
So a conditional type where the type being checked is a naked type parameter is a distributive conditional type. But what does that mean?
When a type function F<T>
is a distributive conditional type, the operation distributes over unions in T
, meaning that it acts on each union member individually and joins the result back into a union. So, for example, F<A | B | C>
will evaluate to the same as F<A> | F<B> | F<C>
.
Furthermore, the never
type is considered to be the empty union, so F<never>
will always be never
no matter what. (see the relevant comment on microsoft/TypeScript#23182 for an authoritative source).
This treatment of never
might be confusing but it is consistent. The never
type gets absorbed into unions: that is, X | never
is reduced to X
no matter what X
is. (If you have a value x
of type X | never
then x
is either of type X
or of type never
, but there are no values of type never
, so x
must be of type X
. So all values assignable to X | never
must be assignable to X
, and the type checker observes this by absorbing never
into unions. See Why is never assignable to every type? for more information.) You can think of never
as the identity element of the union operation.
So if X | never
is equivalent to never
, then F<X>
and F<X | never>
are the same. If F<T>
is distributive, then F<X | never>
is equivalent to F<X> | F<never>
. That implies F<X>
is the same as F<X> | F<never>
for all X
. The easiest way for that to be true is if F<never>
is just never
.
(By analogy, imagine you have a mathematical function () which is distributive over addition, so that () + () = (+). Addition has an identity element: 0, because +0 = . That implies (0) must be 0, because () + (0) = (+0) = (). 0 is "the empty sum", and never
is "the empty union".)
Often distributive behavior is desired, but sometimes it's not. The way to turn that behavior off is therefore to "clothe" both the naked type parameter and the type you're checking against with some covariant type function C<T>
. (See Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more information about variance).
type Distrib<T> = T extends U ? X : Y
type NonDistrib<T> = C<T> extends C<U> ? X : Y
You need to do it to both sides of extends
so that you're doing the same check (if C<T>
is covariant in T
then C<A> extends C<B>
implies A extends B
). Any covariant "clothing" will suffice here.
But if you're looking for the, uh, "skimpiest" clothing so that you can save as many keystrokes as possible, then you should use [T]
. For better or worse, TypeScript considers array types as covariant (see Why are TypeScript arrays covariant? ), and a one-element tuple is an array type. There's nothing more "correct" about using [T]
over any other covariant type function, it's just the most expedient.
And therefore it is usually recommended that if you want to turn off distribution over unions in a conditional type, you wrap both sides of the extends
with [
⋯]
:
type Distrib<T> = T extends U ? X : Y
type NonDistrib<T> = [T] extends [U] ? X : Y