78
votes

I have apple and pears - both have an isDecayed attribute:

interface Apple {
    color: string;
    isDecayed: boolean;
}

interface Pear {
    weight: number;
    isDecayed: boolean;
}

And both types can be in my fruit basket (multiple times):

interface FruitBasket {
   apples: Apple[];
   pears: Pear[];
}

Let's assume for now my basket is empty:

const fruitBasket: FruitBasket = { apples: [], pears: [] };

Now we take randomly one kind out of the basket:

const key: keyof FruitBasket = Math.random() > 0.5 ? 'apples': 'pears'; 
const fruits = fruitBasket[key];

And of course nobody likes decayed fruits so we pick only the fresh ones:

const freshFruits = fruits.filter((fruit) => !fruit.isDecayed);

Unfortunately Typescript tells me:

Cannot invoke an expression whose type lacks a call signature. Type '((callbackfn: (value: Apple, index: number, array: Apple[]) => any, thisArg?: any) => Apple[]) | ...' has no compatible call signatures.

What's wrong here - is it just that Typescript doesn't like fresh fruits or is this a Typescript bug?

You can try it yourself in the official Typescript Repl.

4
Any reason you can't extend a generic Fruit interface with the isDecayed property and then declare fruits to be of type Fruit[]?Gerrit0
strange...there is no error if you explicitly set the key to string i.e const key: string = Math.random() > 0.5 ? 'apples': 'pears';shusson
^ happens because without an explicit index signature, the return type will be any which circumvents the issue.shusson

4 Answers

89
votes

TypeScript supports structural typing (also called duck typing), meaning that types are compatible when they share the same members. Your problem is that Apple and Pear don't share all their members, which means that they are not compatible. They are however compatible to another type that has only the isDecayed: boolean member. Because of structural typing, you don' need to inherit Apple and Pear from such an interface.

There are different ways to assign such a compatible type:

Assign type during variable declaration

This statement is implicitly typed to Apple[] | Pear[]:

const fruits = fruitBasket[key];

You can simply use a compatible type explicitly in in your variable declaration:

const fruits: { isDecayed: boolean }[] = fruitBasket[key];

For additional reusability, you can also define the type first and then use it in your declaration (note that the Apple and Pear interfaces don't need to be changed):

type Fruit = { isDecayed: boolean };
const fruits: Fruit[] = fruitBasket[key];

Cast to compatible type for the operation

The problem with the given solution is that it changes the type of the fruits variable. This might not be what you want. To avoid this, you can narrow the array down to a compatible type before the operation and then set the type back to the same type as fruits:

const fruits: fruitBasket[key];
const freshFruits = (fruits as { isDecayed: boolean }[]).filter(fruit => !fruit.isDecayed) as typeof fruits;

Or with the reusable Fruit type:

type Fruit = { isDecayed: boolean };
const fruits: fruitBasket[key];
const freshFruits = (fruits as Fruit[]).filter(fruit => !fruit.isDecayed) as typeof fruits;

The advantage of this solution is that both, fruits and freshFruits will be of type Apple[] | Pear[].

7
votes

As mentioned in the github issue originally linked by @peter in the comments:

const freshFruits = (fruits as (Apple | Pear)[]).filter((fruit: (Apple | Pear)) => !fruit.isDecayed);
4
votes

Perhaps create a shared Fruit interface that provides isDecayed. fruits is now of type Fruit[] so the type can be explicit. Like this:

interface Fruit {
    isDecayed: boolean;
}

interface Apple extends Fruit {
    color: string;
}

interface Pear extends Fruit {
    weight: number;
}

interface FruitBasket {
    apples: Apple[];
    pears: Pear[];
}


const fruitBasket: FruitBasket = { apples: [], pears: [] };
const key: keyof FruitBasket = Math.random() > 0.5 ? 'apples': 'pears'; 
const fruits: Fruit[] = fruitBasket[key];

const freshFruits = fruits.filter((fruit) => !fruit.isDecayed);
-3
votes

I had the same issue with numeral, a JS library. The fix was to install the typings again with this command:

npm install --save @types/numeral