MrD
MrD

Reputation: 5086

Difference in Generics Declaration

In typescript, is there a difference between

a : T<x> | T<y> 

and

a : T<x | y>

Upvotes: 0

Views: 64

Answers (2)

jcalz
jcalz

Reputation: 328292

This depends entirely on the definition of T, which is not included here. Since the code doesn't seem to be a minimum reproducible example, we can only speak in generalities.

When you say that T<X | Y> is equivalent to T<X> | T<Y> for all X and Y, you are saying that T<X> distributes over unions in its type parameter. In general, T<X> does not distribute over its type parameter. This can happen, but it usually doesn't.

In order to explore some cases, let's introduce some testing equipment:

type Compare<U, V> = [U] extends [V]
  ? ([V] extends [U] ? "mutually assignable" : "subtype")
  : [V] extends [U] ? "supertype" : "unrelated";

The Compare<U, V> type will compare types U and V to see if U is assignable to V and/or vice versa. If T<X | Y> is the same as T<X> | T<Y>, then Compare<T<X | Y>, T<X> | T<Y>> should return "mutually assignable". If not, then you will get one of the other three possibilities: either T<X | Y> is a supertype of T<X> | T<Y> (that is, T<X> | T<Y> is assignable to T<X | Y>), or T<X | Y> is a subtype of T<X> | T<Y> (that is, T<X | Y> is assignable to T<X> | T<Y>), or T<X | Y> is unrelated to T<X> | T<Y> (that is, neither type is assignable to the other).

type X = { a: string; b: number } | number;
type Y = { c: boolean; d: any[] } | string;

I've got some particular example for X and Y here, so in the following tests if T<X | Y> is declared equivalent to T<X> | T<Y>, it's just evidence that it's true for general X and Y, and not proof. Obviously if I pick X and Y to be the same type, then T<X | Y> will always equal T<X> | T<Y>. So I've just chosen a sufficiently distinct X and Y to be illustrative.

Okay, let's get started:


Here are some generic types which are distributive over unions in their type parameter, where T<X> | T<Y> is equivalent to T<X | Y>:

• Constant type functions that don't consult their type parameter are distributive:

type Constant<T> = string;
type ConstantDistributes = Compare<Constant<X | Y>, Constant<X> | Constant<Y>>;
// mutually assignable

• The identity type function is distributive:

type Identity<T> = T;
type IdentityDistributes = Compare<Identity<X | Y>, Identity<X> | Identity<Y>>;
// mutually assignable

• The union of the type parameter with something is distributive:

type OrSomething<T> = T | { z: string };
type OrSomethingDistributes = Compare<
  OrSomething<X | Y>,
  OrSomething<X> | OrSomething<Y>
>; // mutually assignable

• The intersection of the type parameter with something is distributive:

type AndSomething<T> = T & { z: string };
type AndSomethingDistributes = Compare<
  AndSomething<X | Y>,
  AndSomething<X> | AndSomething<Y>
>; // mutually assignable

• A conditional type in which the checked type is the "naked" type parameter (so just T extends ... instead of SomeFunctionOf<T> extends ...) is distributive:

type NakedConditional<T> = T extends object ? { x: T } : { y: T };
type NakedConditionalDistributes = Compare<
  NakedConditional<X | Y>,
  NakedConditional<X> | NakedConditional<Y>
>; // mutually assignable

• A generic type which is bivariant in its type parameter is distributive. This is quite uncommon, since bivariant types are generally unsound. Method parameters in TypeScript are treated as bivariant. Function parameters in general are treated as bivariant if you turn off --strictFunctionTypes... but you shouldn't turn that off.

interface Bivariant<T> {
  method(x: T): void;
}
type BivariantDistributes = Compare<
  Bivariant<X | Y>,
  Bivariant<X> | Bivariant<Y>
>;
// mutually assignable

Those cases were chosen specifically to make the equivalence hold. Now for the more common cases, where they are not equivalent:

• A generic type which is covariant in its type parameter will not distribute. In such types, T<X | Y> will be a supertype of T<X> | T<Y>:

interface Covariant<T> {
  prop: T;
}
type CovariantIsASupertype = Compare<
  Covariant<X | Y>,
  Covariant<X> | Covariant<Y>
>;
// supertype

• A generic type which is contravariant in its type parameter will not distribute. In such types, T<X | Y> will be a subtype of T<X> | T<Y>:

interface Contravariant<T> {
  (arg: T): void;
}
type ContravariantIsASubtype = Compare<
  Contravariant<X | Y>,
  Contravariant<X> | Contravariant<Y>
>;
// subtype

• A generic type which is invariant in its type parameter will not distribute. In such types, T<X | Y> will be unrelated to of T<X> | T<Y>:

interface Invariant<T> {
  (arg: T): T;
}
type InvariantIsUnrelated = Compare<
  Invariant<X | Y>,
  Invariant<X> | Invariant<Y>
>;
// unrelated

Let's recap. Without knowing anything about T, you cannot count on T<X | Y> being equivalent to T<X> | T<Y> for all X and Y. There are some specific cases of T where it is known to distribute over unions, and in those cases there is no difference. Usually, though, there is a difference.

Okay, hope that helps. Good luck!

Link to code

Upvotes: 4

Phillip
Phillip

Reputation: 6253

They are not the same.

a: T<x> | T<y> means that a is of either of type T<x> or of T<y>.

a: T<x | y> means that a is of type T where T is generic over the type x | y.


Any value of type T<x> | T<y> is assignable to any variable of type T<x | y>, but not the other way around.

I made a quick demonstration showcasing this.

Upvotes: 0

Related Questions