基于TypeScript中区分联合的参数自动推断返回类型

问题描述

我正在尝试过滤数组并自动推断返回类型。

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进一步重构为其他类型的签名,因为现有的签名可以按照您的要求工作。

Extract<T,U> utility type