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.
type Fruit = "apple" | "orange";
type Color = "red" | "orange";
type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors
declare let f: FruitEater | ColorConsumer;
// Cannot invoke an expression whose type lacks a call signature.
// Type 'FruitEater | ColorConsumer' has no compatible call signatures.ts(2349)
f("orange");
However, in the above example, both FruitEater
s and ColorConsumer
s 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.
type Fruit = "apple" | "orange";
type Color = "red" | "orange";
type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors
declare let f: FruitEater | ColorConsumer;
f("orange"); // It works! Returns a 'number | string'.
f("apple"); // error - Argument of type '"red"' is not assignable to parameter of type '"orange"'.
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.
interface Dog {
kind: "dog"
dogProp: any;
}
interface Cat {
kind: "cat"
catProp: any;
}
const catOrDogArray: Dog[] | Cat[] = [];
catOrDogArray.forEach(animal => {
// ~~~~~~ error!
// Parameter 'animal' implicitly has an 'any' type.
});
This is still strictly more capable in TypeScript 3.3, and adding an explicit type annotation will work.
interface Dog {
kind: "dog"
dogProp: any;
}
interface Cat {
kind: "cat"
catProp: any;
}
const catOrDogArray: Dog[] | Cat[] = [];
catOrDogArray.forEach((animal: Dog | Cat) => {
if (animal.kind === "dog") {
animal.dogProp;
// ...
}
else if (animal.kind === "cat") {
animal.catProp;
// ...
}
});