根据 TypeScript 中区分联合的参数自动推断 return 类型
Automatically infer return type based on arguments for discriminated union in TypeScript
我正在尝试过滤数组并自动推断 return 类型。
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)
}
所以,我只需要添加类型就可以了。让我们试试:
第一个想法是添加 return 条件类型:
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
的 return 类型现在确实运行良好,但存在两个问题:
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)
}
但是什么 extends
?我没有。
第三个想法是使用重载,但是,这也不是一个好主意,因为它需要手动指定所有类型,就像想法 #1 中那样。
现代TS是否可以只用编译器解决这个问题?
问题不在于 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, and currently such type guard function signatures are not inferred automatically (see microsoft/TypeScript#16069,支持此功能的开放功能请求,了解更多信息)。所以你必须自己注释回调。
为了通用地做到这一点,您可能确实需要条件类型;具体来说,我建议使用 Extract<T, U>
utility type 来表达“可分配给类型 U
的 T
联合的成员”:
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
枚举值)和 returns 一个回调函数,它接受 IItem
i
和 returns 一个 boolean
,编译器可以使用其值来确定 i
是或不是 Extract<IItem, { category: V }>
... 它是“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()
尝试进一步重构为不同的类型签名,因为现有的签名可以按您想要的方式工作。
我正在尝试过滤数组并自动推断 return 类型。
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)
}
所以,我只需要添加类型就可以了。让我们试试:
第一个想法是添加 return 条件类型:
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
的 return 类型现在确实运行良好,但存在两个问题:
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)
}
但是什么 extends
?我没有。
第三个想法是使用重载,但是,这也不是一个好主意,因为它需要手动指定所有类型,就像想法 #1 中那样。
现代TS是否可以只用编译器解决这个问题?
问题不在于 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, and currently such type guard function signatures are not inferred automatically (see microsoft/TypeScript#16069,支持此功能的开放功能请求,了解更多信息)。所以你必须自己注释回调。
为了通用地做到这一点,您可能确实需要条件类型;具体来说,我建议使用 Extract<T, U>
utility type 来表达“可分配给类型 U
的 T
联合的成员”:
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
枚举值)和 returns 一个回调函数,它接受 IItem
i
和 returns 一个 boolean
,编译器可以使用其值来确定 i
是或不是 Extract<IItem, { category: V }>
... 它是“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()
尝试进一步重构为不同的类型签名,因为现有的签名可以按您想要的方式工作。