如何使用 TypeScript 过滤不同类型的数组并指定明确类型?
我有一个类,它有两个字段。这些字段是不同类型的数组,但它们有共同的字段id。所以,我想实现一个方法,根据提供的id过滤数组并移除对应的项。
enum ItemType {
VEGETABLES = 'vegetables',
FRUITS = 'fruits'
}
class BaseVegetable {
id: string = '';
color: string = '';
}
class BaseFruit {
id: string = '';
description?: any;
}
class Basket {
vegetables: BaseVegetable[] = [];
fruits: BaseFruit[] = [];
removeItem(id: string, type: ItemType) {
this[type] = this[type].filter(item => id !== item.id);
}
}
TypeScript Playground 中的这个示例
现在我收到 TypeScript 错误了
此表达式不可调用。联合类型 '{ (predicate: (value: BaseVegetable, index: number, array: BaseVegetable[]) => value is S, thisArg?: any): S[]; (predicate: (value: BaseVegetable, index: number, array: BaseVegetable[]) => unknown, thisArg?: any): BaseVegetable[]; } | { ...; }' 的每个成员都有签名,但这些签名之间没有一个是相互兼容的。(2349)
在过滤函数中的 item 类型是 any。但是如果我写类似 const a=this[type] 的代码,我会得到正确的类型 BaseVegetable[] | BaseFruit[]。
你能帮我弄清楚这个错误的原因吗?是否可以以这种方式实现 remove 函数,并用 TS 正确描述它?
答案#1
该错误发生是因为 TypeScript 无法跟踪多个联合类型表达式之间的关联,如 microsoft/TypeScript#30581 所述。
当 TypeScript 分析以下代码时:
this[type] = this[type].filter(item => id !== item.id)
它看到 this[type] 是 BaseVegetable[] | BaseFruit[],一个数组类型的联合,并且在联合类型上调用 filter 会有问题(参见 microsoft/TypeScript#44373)。即使你能解决这个问题,它也只会看到右侧是 BaseVegetable[] | BaseFruit[],而将其写入 this 的类型为 ItemType 的属性可能是不合适的,如 microsoft/TypeScript#30769 所强制执行的那样。编译器在一般情况下阻止人们这样做是正确的,如下所示:
demo(typeRead: ItemType, typeWrite: ItemType) {
this[typeWrite] = this[typeRead]; // this fails because it's not safe
}
因为如果你写的是一个与你读取的不同的属性怎么办?你知道你的代码是安全的原因是你在每一边使用了一个相同的变量type。但是 TypeScript 在这里并不检查身份,它检查的是类型。
本质上,TypeScript 只能一次检查一段代码。编译器不会投机性地将 type 缩小到 ItemType.VEGETABLES,然后再投机性地将 type 缩小到 ItemType.FRUITS 并检查每种情况。如果它这样做,那么编译器就能看到在两种情况下都能工作。但它并不会这样做,因为如果编译器自动将每段代码检查多次(次数等于所有可能联合类型缩小的数量),除了最简单的程序之外,这会导致编译器性能灾难性下降。也许如果开发者可以按需选择这种方式,它可能奏效,但这一方法已在 microsoft/TypeScript#25051 中提出并被拒绝。
要解决该错误,您可以遵循 microsoft/TypeScript#47109 中描述的建议。其思路是将您的操作从联合类型重构为泛型,因为泛型允许您编写一个代码块,同时表示一系列情况。而且编译器在分析泛型时要好得多,尤其是对映射类型中的泛型索引访问。
这是你示例的样子:
class Basket {
vegetables: BaseVegetable[] = [];
fruits: BaseFruit[] = [];
removeItem<K extends ItemType>(id: string, type: K) {
const thiz: { [P in K]: Basket[P][number][] } = this; // okay
thiz[type] = thiz[type].filter(item => id !== item.id); // okay
}
}
我们在 K 中将 removeItem 泛型化,即 type 的类型。当 type 在函数中出现多次时,编译器更倾向于将泛型类型 K 视为在赋值的左右两侧是“相同的”。这将允许 this[type]=this[type]; 正常工作,但过滤操作则不行。
为了让编译器理解过滤操作,我们需要 this[type] 本身是形式为 F<K>[] 的泛型,其中 F 是某个类型。我们可以通过将 this 重写为映射类型来实现:{ [P in K]: Basket[P][number][] },意思是:this 的每个 ItemType 键属性都是对应 Basket 属性数组元素的数组。这有点像同义反复。因此,当 K 是 ItemType 时,你会得到 {vegetables: Basket["vegetables"][number][], fruits: Basket["fruits"][number][]},因此得到 {vegetables: BaseVegetable[], fruits: BaseFruit[]}。编译器对将 this 指派给该类型感到满意,所以它允许这样做。
const thiz: { [P in K]: Basket[P][number][] } = this; // okay
有人可能认为这并不是改进,但重构该类型可以让编译器明确地将 thiz[type] 视为一个在 K 上泛型的单一数组类型:
const thizType: Basket[K][number][] = thiz[Type];
因此你可以使用 filter() 对其进行过滤,并在赋值的两边都得到 Basket[K][number][],编译器会接受:
thiz[type] = thiz[type].filter(item => id !== item.id); // okay
所以这就是你可以在从编译器获得某种类型安全的同时修复它的方法。
当然,对于你来说,这种重构可能不值得。如果你只是想抑制错误并继续前进,你可以使用类型断言来说服编译器你在做正确的事情(即使你说服它的方式是假的):
removeItem(id: string, type: ItemType) {
this[type] = (this[type] as
{ id: string }[]).filter(item => id !== item.id
) as (BaseVegetable & BaseFruit)[];
}
这行得通;说 this[type] 是一个 {id: string}[] 或多或少是对的,但说过滤结果是一个 (BaseVegetable & BaseFruit)[] 就是假的。无论哪种方式,编译器都会接受。这不会捕捉到错误(如果你把 this[type]= 改成 this.vegetables =,编译器也不会注意到),所以你需要在这方面更加小心。
代码的 Playground 链接
