[英]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[]'
第二個想法是添加某種約束,如下所示:
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>
實用程序類型來表達“可分配給類型U
的T
聯合成員”:
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()
的不同類型簽名,因為現有的簽名可以按照您的意願工作。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.