问题描述
我正在尝试过滤数组并自动推断返回类型。
enum Category {
Fruit,Animal,Drink,}
interface IApple {
category: Category.Fruit
taste: string
}
interface ICat {
category: Category.Animal
name: string
}
interface ICocktail {
category: Category.Drink
price: number
}
type IItem = IApple | ICat | ICocktail
const items: IItem[] = [
{ category: Category.Drink,price: 30 },{ category: Category.Animal,name: 'Fluffy' },{ category: Category.Fruit,taste: 'sour' },]
所以现在我想过滤items
,就像这样:
// return type is IItem[],but I want it to be IFruit[]
items.filter(x => x.category === Category.Fruit)
我知道Array#filter
太普通了,不能这样做,所以我试图将其包装在自定义函数中:
const myFilter = (input,type) => {
return input.filter(x => x.category === type)
}
所以,我所需要的只是添加类型,这很好。让我们尝试一下:
const myFilter = <X extends IItem,T extends X['category']>(
input: X[],type: T
): T extends Category.Fruit ? IApple[] : T extends Category.Drink ? ICocktail[] : ICat[] => {
// TS error here
return input.filter((x) => x.category === type)
}
虽然myFilter
的返回类型确实可以很好地工作,但是存在两个问题:
-
input.filter((x) => x.category === type)
被突出显示为错误:Type 'X[]' is not assignable to type 'T extends Category.Fruit ? IApple[] : T extends Category.Drink ? ICocktail[] : ICat[]'
- 我手动指定了所有可能的情况,基本上是编译器应该做的工作。当我只有3个类型的相交时,这很容易做到,但是当有20个类型的相交时……就不那么容易了。
第二个想法是添加某种约束,例如:
const myFilter = <X extends IItem,T extends X['category'],R extends ...>(input: X[],type: T): X[] => {
return input.filter(x => x.category === type)
}
但是R extends
是什么?我不知道。
第三个想法是使用重载,但这也不是一个好主意,因为它需要手动指定所有类型,就像在想法#1中一样。
在现代TS中是否可以仅使用编译器来解决此问题?
解决方法
问题不在Array.prototype.filter()
上,interface Array<T> {
filter<S extends T>(
predicate: (value: T,index: number,array: T[]) => value is S,thisArg?: any
): S[];
}
的{{3}}实际上具有一个呼叫签名,可用于根据回调来缩小返回数组的类型:
T
问题在于此调用签名要求回调必须为typings in the standard TS library,并且当前不会自动推断出此类类型保护功能签名(请参阅user-defined type guard function,对此功能的开放功能请求,请参见更多信息)。因此,您必须自己注释回调。
为了通用地做到这一点,您可能确实需要条件类型。具体来说,我建议使用microsoft/TypeScript#16069表示“可分配给类型U
的{{1}}联合的成员”:
const isItemOfCategory =
<V extends IItem['category']>(v: V) =>
(i: IItem): i is Extract<IItem,{ category: V }> =>
i.category === v;
在这里,isItemOfCategory
是一种咖喱函数,其值类型为v
的值V
可分配给IItem['category']
(即Category
枚举之一)值),然后返回一个回调函数,该回调函数接受一个IItem
i
并返回一个boolean
,编译器可使用该值确定i
是否为{{1 }} ...是“ Extract<IItem,{ category: V }>
属性的类型为IItem
的{{1}}联合的成员”。让我们看看它的作用:
category
看起来不错。我认为不需要为V
进一步重构为其他类型的签名,因为现有的签名可以按照您的要求工作。