数组联合的方法签名合并问题
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 和IHotel。
这实际上是不可能的,因此编译器得出结论,由于类型签名不可满足,它也是不可调用的。
这是方法签名合并方式的一个弱点。这是合并签名的正确方法,但对于许多用例,结果类型不是您实际需要的,方法调用也不是不可满足的。它更有限满足它但绝对不是不可能:
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[] | IHotel[](IShop 数组或IHotel 数组)导致无法调用的方法签名合并,我们可以将类型更改为(IShop | IHotel)[](IShop 和IHotel 项的数组)。这有点不正确,因为您没有混合数组。但是,在实践中几乎没有区别。您仍然需要知道每个项目是什么,因此这与拥有任一类型的数组非常相似。
使它起作用的是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 和IHotel 扩展它。这样你就可以更直接地说allRegions 将包含超类型。结果与联合类型IShop | IHotel 基本相同,但更加明确。
const allRegions: IDataItem[] = shops.length > 0 ? shops : hotels;
allRegions.find(r => r.name === 'name'); //allowed
Playground Link
如果您的数据实际上是相关的,则最好在您的类型中表示它。类型联合不传达有关关系的信息。但是,这仍然需要您能够更改类型。如果这不可能,那么类型联合是更好的选择。
创建一个新的联合,确保数组方法可用
作为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 | IHotel 数组相交
- 同质
IHotel 数组与混合IShop | IHotel 数组相交
精彩之处在于它的行为与IShop[] | IHotel[] 完全相同 - 你不能混合使用。但是,与此同时,类型将确保方法声明合并正常工作。这意味着您可以对仅包含一种类型的项目但未混合的数组进行正确的类型检查:
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
附录:用allRegions初始化澄清
const allRegions = shops.length > 0 ? shops : (hotels.length > 0 ? hotels : []) 这一行是多余的。你只分配一个空数组给allRegions 是hotels 是一个空数组(和shops 也是)。由于空数组在任何情况下都是空数组,因此您可以将其缩短为 const allRegions = shops.length > 0 ? shops : hotels - 如果 hotels 为空,则无论如何您都是空数组。这是我在上面的代码示例中使用的,因为它使代码更易于阅读。
只要您不打算就地改变数组,它就会产生完全相同的效果。这可能会修改错误的数组。