Improved behavior for calling union types

In prior versions of TypeScript, unions of callable types could only be invoked if they had identical parameter lists.

  1. type Fruit = "apple" | "orange";
  2. type Color = "red" | "orange";
  3. type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
  4. type ColorConsumer = (color: Color) => string; // consumes and describes the colors
  5. declare let f: FruitEater | ColorConsumer;
  6. // Cannot invoke an expression whose type lacks a call signature.
  7. // Type 'FruitEater | ColorConsumer' has no compatible call signatures.ts(2349)
  8. f("orange");

However, in the above example, both FruitEaters and ColorConsumers should be able to take the string "orange", and return either a number or a string.

In TypeScript 3.3, this is no longer an error.

  1. type Fruit = "apple" | "orange";
  2. type Color = "red" | "orange";
  3. type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
  4. type ColorConsumer = (color: Color) => string; // consumes and describes the colors
  5. declare let f: FruitEater | ColorConsumer;
  6. f("orange"); // It works! Returns a 'number | string'.
  7. f("apple"); // error - Argument of type '"red"' is not assignable to parameter of type '"orange"'.
  8. f("red"); // error - Argument of type '"red"' is not assignable to parameter of type '"orange"'.

In TypeScript 3.3, the parameters of these signatures are intersected together to create a new signature.

In the example above, the parameters fruit and color are intersected together to a new parameter of type Fruit & Color.Fruit & Color is really the same as ("apple" | "orange") & ("red" | "orange") which is equivalent to ("apple" & "red") | ("apple" & "orange") | ("orange" & "red") | ("orange" & "orange").Each of those impossible intersections reduces to never, and we’re left with "orange" & "orange" which is just "orange".

Caveats

This new behavior only kicks in when at most one type in the union has multiple overloads, and at most one type in the union has a generic signature.That means methods on number[] | string[] like map (which is generic) still won’t be callable.

On the other hand, methods like forEach will now be callable, but under noImplicitAny there may be some issues.

  1. interface Dog {
  2. kind: "dog"
  3. dogProp: any;
  4. }
  5. interface Cat {
  6. kind: "cat"
  7. catProp: any;
  8. }
  9. const catOrDogArray: Dog[] | Cat[] = [];
  10. catOrDogArray.forEach(animal => {
  11. // ~~~~~~ error!
  12. // Parameter 'animal' implicitly has an 'any' type.
  13. });

This is still strictly more capable in TypeScript 3.3, and adding an explicit type annotation will work.

  1. interface Dog {
  2. kind: "dog"
  3. dogProp: any;
  4. }
  5. interface Cat {
  6. kind: "cat"
  7. catProp: any;
  8. }
  9. const catOrDogArray: Dog[] | Cat[] = [];
  10. catOrDogArray.forEach((animal: Dog | Cat) => {
  11. if (animal.kind === "dog") {
  12. animal.dogProp;
  13. // ...
  14. }
  15. else if (animal.kind === "cat") {
  16. animal.catProp;
  17. // ...
  18. }
  19. });