Reputation: 2914
I have two array variables with following interface:
export interface IShop {
name: string,
id: number,
type: string,
}
export interface IHotel {
name: string,
id: number,
rooms: number,
}
My typescript code is as following:
let shops: IShop[];
let hotels: IHotel[];
//these variables then gets assigned respective data from an API matching the interfaces
const allRegions = shops.length > 0 ? shops : (hotels.length > 0 ? hotels : []);
allRegions.find(r => r.name === 'name');
at the last line, I get error saying:
Cannot invoke an expression whose type lacks a call signature. Type '{ (predicate: (this: void, value: IShop, index: number, obj: IShop[]) => value is S, thisArg?: any): S; (predicate: (value: IShop, index: number, obj: IShop[]) => boolean, thisArg?: any): IShop; } | { ...; }' has no compatible call signatures.
Same is happening for other Array methods during compilation, though code works fine and I know what the issue means but I am not clear on why Typscript is not recognizing the Array.
On typechecking allRegion
, i get IShop[] | IHotel[]
both of which are clearly arrays, is there something wrong with the datatype of allRegion
?
Upvotes: 2
Views: 794
Reputation: 29096
The reason TypeScript complains is because with a type of IShop[] | IHotel[]
it will merge all method signatures. In particular the signatures:
Array<IShop>.find(
predicate: (
value: IShop,
index: number,
obj: IShop[]
) => unknown, thisArg?: any
): IShop | undefined
Array<IHotel>.find(
predicate: (
value: IHotel,
index: number,
obj: IHotel[]
) => unknown, thisArg?: any
): IHotel | undefined
Effectively becomes something similar to:
Array<IShop & IHotel>.find(
predicate: (
value: IShop & IHotel,
index: number,
obj: (IShop & IHotel)[]
) => unknown, thisArg?: any
): IShop & IHotel | undefined
This means that in order to call it, the callback should accept an item that's both IShop
and IHotel
at the same time and also will produce both an IShop
and IHotel
at the same time.
That's not actually possible, thus the compiler concludes that as the type signature is unsatisfiable, it is also uncallable.
This is a bit of a weakness in the way of how the method signatures are merged. It is the correct way to merge the signatures but for many use cases, the resulting types are not what you actually need, nor is the method call unsatisfiable. It's more limited in what can satisfy it but definitely not impossible:
let shops = [{name: "shop1", id: 1, type: "supermarket"}];
let hotels = [{name: "hotel1", id: 2, rooms: 42}];
// see addendum
const allRegions = shops.length > 0 ? shops : hotels;
const result = allRegions.find(r => r.name === 'shop1');
console.log(result);
The issue is that this serves a more localised case and not the more general case where for any variation of calling the method.
The way to go around it is to use explicit typing which will allow you to retain type safety but you have to slightly override the compiler's decision.
Since IShop[] | IHotel[]
(an array of IShop
or array of IHotel
) causes method signature merges that is uncallable, we can change the type to (IShop | IHotel)[]
(an array of IShop
and IHotel
items). This is slightly incorrect, as you don't have a mixed array. However, there is almost no difference in practice. You still need to know what each item is, so it's very similar to having an array of either type.
What makes it work is that IShop | IHotel
will allow you to use the shared properties between the two interfaces. In this case, name
and id
. Therefore, TypeScript will allow the call like allRegions.find(r => r.name === 'name')
.
const allRegions: (IShop | IHotel)[] = shops.length > 0 ? shops : hotels;
allRegions.find(r => r.name === 'name'); //allowed
Very similar to the above but you'd need to change your types:
interface IDataItem {
name: string,
id: number,
}
export interface IShop extends DataItem {
type: string,
}
export interface IHotel extends IDataItem {
rooms: number,
}
This is extracting the shared properties to an interface and then both IShop
and IHotel
extend it. This way you can more directly say that allRegions
will contain the supertype. The result is essentially the same as the union type IShop | IHotel
but made more explicit.
const allRegions: IDataItem[] = shops.length > 0 ? shops : hotels;
allRegions.find(r => r.name === 'name'); //allowed
If your data is actually related it might be preferable to represent that in your types. The type union does not convey the information about the relation. However, this still requires you to be able to change the types. If that's not a possibility, then a type union is the better option.
As a brilliant suggestion in a comment from Linda Paiste:
it's possible to declare
const allRegions: (IShop[] | IHotel[]) & (IShop | IHotel)[]
so that we get the union signature without losing the restriction that the array elements are of the same type.
Which will give you this:
const allRegions: (IShop[] | IHotel[]) & (IShop | IHotel)[] = shops.length > 0 ? shops : hotels;
allRegions.find(r => r.name === 'name'); //allowed
This is an intersection between two homogenous arrays and a mixed array.
This declaration resolves to (IShop[] & (IShop | IHotel)[]) | (IHotel[] & (IShop | IHotel)[])
which is a union of
IShop
array intersected with a mixed IShop | IHotel
arrayIHotel
array intersected with a mixed IShop | IHotel
arrayThe brilliant part is that it behaves exactly the same as IShop[] | IHotel[]
- you cannot have a mix. However, at the same time, the type will ensure the method declaration merge works correctly. This means that you get correct type checks for arrays that only have one type of item in them but not mixed:
declare let shops: IShop[];
declare let hotels: IHotel[];
//mixed array
declare let mixed: (IShop | IHotel)[];
//homogenous array of either type
declare let improved: (IShop[] | IHotel[]) & (IShop | IHotel)[];
//something that takes a homogenous array
declare function foo(x: IShop[] | IHotel[]): void;
foo(shops); //ok
foo(hotels); //ok
foo(mixed); //error
foo(improved); //ok
allRegions
initialisationThe line const allRegions = shops.length > 0 ? shops : (hotels.length > 0 ? hotels : [])
is superfluous. You only assign an empty array to allRegions
is hotels
is an empty array (and shops
too). Since an empty array is an empty array either case, you can shorten this to const allRegions = shops.length > 0 ? shops : hotels
- if hotels
is empty, you an empty array anyway. This is what I've used in the code samples above as it makes the code a lot easier to read.
It has the exact same effect as long as you don't plan on mutating the array in-place. That might modify the wrong array.
Upvotes: 3
Reputation: 2611
Add Explicit typing to allRegions
, You are getting this error because ht empty array that you are passing at the end of the condition don't have any type.
const allRegions: IShop[] | IHotel[] | any[] = shops.length > 0 ? shops : (hotels.length > 0 ? hotels : []);
Upvotes: -2