数组并集的方法签名合并问题
TypeScript抱怨的原因是
IShop[] | IHotel[]
它将合并所有方法签名。特别是签名:
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
实际上,它类似于:
Array<IShop & IHotel>.find(
predicate: (
value: IShop & IHotel,
index: number,
obj: (IShop & IHotel)[]
) => unknown, thisArg?: any
): IShop & IHotel | undefined
这意味着,为了调用它,回调函数应该接受一个同时满足以下条件的项目
IShop
和
IHotel
同时也将产生
IShop
和
IH酒店
同时。
这实际上是不可能的,因此编译器得出结论,由于类型签名不可满足,它也是不可调用的。
这是方法签名合并方式的一个弱点。这是合并签名的正确方法,但对于许多用例,结果类型不是您实际需要的,方法调用也不是不可满足的。它是
更有限
什么可以满足它,但绝对不是不可能的:
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);
问题是,这适用于更局部的情况,而不是更一般的情况,即调用方法的任何变体。
解决这个问题的方法是使用显式类型,这将允许您保持类型安全,但您必须稍微覆盖编译器的决定。
可能的解决方案
从数组的并集更改为并集类型的数组
自从
IShop[]| I酒店[]
(一系列
IShop
或数组
IH酒店
)导致方法签名合并不可调用,我们可以将类型更改为
(IShop | IHotel)[]
(一系列
IShop
和
IH酒店
项目)。这有点不正确,因为您没有混合数组。然而,在实践中几乎没有区别。你仍然需要知道每个项目是什么,所以这与拥有任何一种类型的数组非常相似。
它之所以有效,是因为
IShop | IHotel
将允许您使用两个接口之间的共享属性。在这种情况下,
name
和
id
因此,TypeScript将允许如下调用
allRegions.find(r => r.name === 'name')
.
const allRegions: (IShop | IHotel)[] = shops.length > 0 ? shops : hotels;
allRegions.find(r => r.name === 'name'); //allowed
Playground Link
引入超级类型
与上述非常相似,但您需要更改类型:
interface IDataItem {
name: string,
id: number,
}
export interface IShop extends DataItem {
type: string,
}
export interface IHotel extends IDataItem {
rooms: number,
}
这是将共享属性提取到接口,然后两者都提取
IShop
和
IH酒店
扩展它。这样你就可以更直接地说
allRegions
将包含超类型。结果与联合类型基本相同
IShop |酒店
但更加明确。
const allRegions: IDataItem[] = shops.length > 0 ? shops : hotels;
allRegions.find(r => r.name === 'name'); //allowed
Playground Link
如果你的数据实际上是相关的,那么最好在你的类型中表示出来。类型联合不传达有关关系的信息。但是,这仍然需要您能够更改类型。如果这不可能,那么类型联合是更好的选择。
创建一个新的联合,以确保可用的数组方法
As
a brilliant suggestion in a comment
从
Linda Paiste
:
可以申报
const allRegions: (IShop[] | IHotel[]) & (IShop | IHotel)[]
这样我们就可以在不失去数组元素类型相同的限制的情况下获得联合签名。
这将给你:
const allRegions: (IShop[] | IHotel[]) & (IShop | IHotel)[] = shops.length > 0 ? shops : hotels;
allRegions.find(r => r.name === 'name'); //allowed
Playground Link
这是两个同质阵列和混合阵列之间的交集。
本声明决定
(IShop[] & (IShop | IHotel)[]) | (IHotel[] & (IShop | IHotel)[])
这是一个联盟
-
同质
IShop
阵列与混合阵列相交
IShop |酒店
阵列
-
同质
IH酒店
阵列与混合阵列相交
IShop |酒店
阵列
最棒的是,它的行为与
IShop[]| I酒店[]
-你不能混合。然而,与此同时,该类型将确保方法声明合并正确工作。这意味着您可以对只有一种类型的项但没有混合的数组进行正确的类型检查:
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
Playground Link
附录:澄清
所有地区
初始化
线
const allRegions = shops.length > 0 ? shops : (hotels.length > 0 ? hotels : [])
是多余的。您只能将空数组分配给
所有地区
是
hotels
是一个空数组(和
shops
也)。由于空数组在任何一种情况下都是空数组,因此可以将其缩短为
const allRegions = shops.length > 0 ? shops : hotels
-如果
酒店
如果是空的,那么你仍然是一个空数组。这就是我在上面的代码示例中使用的,因为它使代码更容易阅读。
只要你不打算在原地对数组进行突变,它就会产生完全相同的效果。这可能会修改错误的数组。