4

In this example:

type Action<T extends string, P = never> = {
  type: T;
} & ([P] extends [never]
  ? {}
  : {
      payload: P;
    });

Why do we need square brackets around conditional [P] extends [never] and what is the difference if we don't use it like this P extends never?

I understand what this type doing, just can't find explanation on why do we actually need square brackets and why it doesn't work without them.

EDIT: Let me try with more simple example:


type GetTest<T = never> = [T] extends [never] ? string : number;

type Test = GetTest; // type Test = string; 

Here is type Test = string as expected. But what I didn't get is what happen with condition when we don't have square brackets, we doesn't get string but we don't get a number too, instead we get type Test = never

type GetTest<T = never> = T extends never ? string : number;

type Test = GetTest; // type Test = never

land
  • 53
  • 5
  • 1
    `[T]` describes a tuple with a single element of type `T` such as `[never]`. – Aluan Haddad Aug 17 '23 at 15:52
  • 2
    @AluanHaddad I think OP gets that and is asking why you need to wrap it in a tuple in the first place. – Jared Smith Aug 17 '23 at 15:53
  • @JaredSmith does he in fact need to do that? – Aluan Haddad Aug 17 '23 at 15:54
  • @jcalz I look into distributive conditional types, thanks for that. Still I do not understand, for example if we doesn't use square brackets: ``` type Action = { type: T; } & (P extends never ? {} : { payload: P; }); type CounterAction = Action<'increment' | 'decrement'>; type UpdateCounterAction = Action<'update', number | string>; ``` Why do we get `type CounterAction = never`? I get now distributive condition for `UpdateCounterAction` and why we need it for this part. – land Aug 17 '23 at 16:48
  • 1
    Here's a SO answer which does a decent job of explaining it: https://stackoverflow.com/a/65492934/264794. And here's a link to a github discussion on the behavior: https://github.com/microsoft/TypeScript/issues/23182 – Ryan Wheale Aug 17 '23 at 16:57
  • It work without square brackets. – ClusterH Aug 17 '23 at 16:58
  • @jcalz checkout edit – land Aug 17 '23 at 17:13

1 Answers1

2

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
jcalz
  • 264,269
  • 27
  • 359
  • 360