【问题标题】:Automatically infer return type based on arguments for discriminated union in TypeScript根据 TypeScript 中区分联合的参数自动推断返回类型
【发布时间】:2021-02-21 19:09:18
【问题描述】:

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

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) =&gt; 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 中是否有可能只使用编译器来解决这个问题?

【问题讨论】:

    标签: typescript discriminated-union


    【解决方案1】:

    问题不在于Array.prototype.filter(),其typings in the standard TS library 实际上确实有一个调用签名,可用于根据回调缩小返回数组的类型:

    interface Array<T> {
      filter<S extends T>(
        predicate: (value: T, index: number, array: T[]) => value is S, 
        thisArg?: any
      ): S[];
    }
    

    问题在于,此调用签名要求回调为user-defined type guard function,目前此类类型保护函数签名不会自动推断(请参阅microsoft/TypeScript#16069,支持此功能的开放功能请求,了解更多信息)。所以你必须自己注释回调。

    为了一般地做到这一点,你可能需要条件类型;具体来说,我建议使用Extract&lt;T, U&gt; utility type 来表达“T 联合的成员可分配给U 类型”:

    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 是否为Extract&lt;IItem, { category: V }&gt;... 这是“IItem 联合的成员,其category属性的类型为V"。让我们看看它的实际效果:

    console.log(items.filter(isItemOfCategory(Category.Fruit)).map(x => x.taste)); // ["sour"]
    console.log(items.filter(isItemOfCategory(Category.Drink)).map(x => x.price)); // [30]
    console.log(items.filter(isItemOfCategory(Category.Animal)).map(x => x.name)); // ["Fluffy"]
    

    看起来不错。我认为没有必要尝试进一步重构为 filter() 的不同类型签名,因为现有的签名可以按照您的意愿工作。

    Playground link to code

    【讨论】:

    • 太棒了!我的实际问题由多个嵌套的 flatMap 组成,因此我将不得不进一步重构,但这是一个了不起的起点。谢谢!
    猜你喜欢
    • 1970-01-01
    • 2020-02-28
    • 2021-10-05
    • 2020-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-23
    • 2019-12-06
    相关资源
    最近更新 更多