如何使用TypeScript过滤不同类型的数组并指定显式类型?

bzzcjhmw  于 2023-04-22  发布在  TypeScript
关注(0)|答案(1)|浏览(154)

我有一个有两个字段的类。这两个字段是不同类型的数组,但它们有一个公共字段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);
  }
}

这个例子在TSPlaygroundPlayground
现在我得到了 typescript 错误
此表达式不可调用。联合类型“{( predicate :(value:BaseVegetable,index:数字,数组:BaseVegetable[])=〉value is S,thisArg?:any):S[];(predicate:(value:BaseVegetable,index:数字,数组:BaseVegetable[])=〉unknown,thisArg?:int [];}|{...; }'具有签名,但这些签名彼此不兼容。(2349)
filter函数中的item的类型是any,但是如果我写const a = this[type],我会得到正确的类型BaseVegetable[] | BaseFruit[]
你能帮助我理解错误的原因吗?是否可以以这种方式实现remove功能并用TS正确描述它?

vxf3dgd4

vxf3dgd41#

发生错误是因为TypeScript无法遵循多个union类型表达式之间的 correlations,如microsoft/TypeScript#30581中所述。
当TypeScript分析以下代码时:

this[type] = this[type].filter(item => id !== item.id)

它发现this[type]BaseVegetable[] | BaseFruit[],是数组类型的联合,在联合上调用filter时存在问题(参见microsoft/TypeScript#44373)。即使你可以解决这个问题,它也只会看到右手边是BaseVegetable[] | BaseFruit[],这可能不适合在ItemType类型的键处写入this的属性,正如microsoft/TypeScript#30769中强制执行的那样。编译器通常会阻止人们这样做,如下所示:

demo(typeRead: ItemType, typeWrite: ItemType) {    
  this[typeWrite] = this[typeRead]; // this fails because it's not safe
}

因为如果你写的是一个和你读的不同的属性呢?你知道你的代码是安全的原因是你在每一边都使用了一个 * 相同 * 的变量type。但是TypeScript在这里不检查 identity,而是检查 type
从本质上讲,TypeScript只能检查一次单个代码块。编译器不会推测性地将type缩小到ItemType.VEGETABLES,然后推测性地将type缩小到ItemType.FRUITS并检查每种情况。如果它这样做了,那么编译器可以看到它在两种情况下都工作。但它没有,因为如果编译器自动检查每个代码块的次数与所有可能的并集缩小的次数一样多,则对于除了最简单的程序之外的所有程序,这将导致灾难性的糟糕编译器性能。如果开发人员可以根据需要选择它,也许它会起作用,但这种方法在microsoft/TypeScript#25051中被建议并拒绝。
要解决这个错误,你可以遵循microsoft/TypeScript#47109中描述的建议。这个想法是重构你的操作,远离union,朝着generics,因为泛型允许你编写一个单独的代码块,一次代表一系列的情况。编译器在分析泛型方面要好得多,特别是将泛型indexed accesses转换为mapped types
下面是您的示例的外观:

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>[]形式的泛型。我们可以通过将this重写为Map类型来做到这一点:{ [P in K]: Basket[P][number][] }表示:this的每个属性ItemType键都是Basket的相应属性的数组元素的数组。这是一种重言式。所以当KItemType时,你得到{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

所以这就是你可以修复它的方法,同时从编译器获得一些类型安全。
当然,这种重构对你来说可能不值得。如果你只是想抑制错误并继续前进,你可以使用类型Assert来说服编译器你正在做正确的事情(即使你说服它的方式是谎言):

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链接到代码

相关问题