簡體   English   中英

根據 TypeScript 中可區分聯合的參數自動推斷返回類型

[英]Automatically infer return type based on arguments for discriminated union in 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[]' 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() ,其在標准 TS 庫中的類型實際上確實有一個調用簽名,可用於根據回調縮小返回數組的類型:

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

問題是這個調用簽名要求回調是用戶定義的類型保護函數,目前這種類型保護函數簽名不會自動推斷(請參閱microsoft/TypeScript#16069 ,支持此功能的開放功能請求,了解更多信息)。 所以你必須自己注釋回調。

為了一般地做到這一點,您可能確實需要條件類型; 具體來說,我建議使用Extract<T, U>實用程序類型來表達“可分配給類型UT聯合成員”:

const isItemOfCategory =
  <V extends IItem['category']>(v: V) =>
    (i: IItem): i is Extract<IItem, { category: V }> =>
      i.category === v;

此處, isItemOfCategory是一個isItemOfCategory函數,它采用可分配給IItem['category'] (即Category枚舉值之一)的V類型值v並返回一個回調函數,該回調函數采用IItem i並返回一個boolean其值編譯器可以用來確定i是否為Extract<IItem, { category: V }> ... 它是“ category屬性為V類型的IItem聯合的成員”。 讓我們看看它的實際效果:

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 鏈接到代碼

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM