我需要提供一个接受一个对象并返回给定类型值的适配器函数。我正在尝试编写一个通用函数,该函数将在给定对象属性名称的情况下生成此类适配器函数,其中属性名称仅限于其值是给定类型的属性。
例如,考虑数字属性的简单情况:
type Adapter<T> = (from: T) => number
我想我想做些类似的事情:
type NumberFields<T> = {[K in keyof T]: T[K] extends number ? K : never}[keyof T]
function foo<T>(property: NumberFields<T>) {
return function(from: T) {
return from[property]
}
}
当NumberFields
的参数不是泛型时,TypeScript会很高兴:
interface Foo {
bar: number
}
function ok<T extends Foo, K extends NumberFields<Foo>>(property: K): Adapter<T> {
return function(from: T): number {
return from[property] // ok
}
}
但是,在一般情况下,TypeScript不想认识到from[property]
只能是数字:
function bar<T, K extends NumberFields<T>>(property: K): Adapter<T> {
return function(from: T): number {
return from[property] // error: Type 'T[K]' is not assignable to to type 'number'
}
}
function baz<T, K extends NumberFields<T>>(property: K): Adapter<T> {
return function(from: T): T[K] { // error: Type '(from: T) => T[K]' is not assignable to type 'Adapter<T>'
return from[property]
}
}
为什么在类型受约束的情况下有效,但在真正通用的情况下却不起作用?
我看过很多SO问题,其中Narrowing TypeScript index type似乎最接近我想要的问题,但是类型保护在这里没有用,因为类型应该是通用的。
答案 0 :(得分:1)
编译器还不够聪明,无法通过通用类型参数的这种类型操作来查看。对于具体类型,编译器可以将最终类型作为具体属性袋进行评估,因此它知道输出将为Foo['bar']
类型,即number
类型。
这里有不同的处理方法。通常最方便的是使用type assertion,因为您比编译器更聪明。这使发出的JavaScript保持相同,但是只是告诉编译器不要担心:
function bar<T, K extends NumberFields<T>>(property: K): Adapter<T> {
return function (from: T): number {
return from[property] as unknown as number; // I'm smarter than the compiler
}
}
当然,它不是类型安全的,因此您需要注意不要对编译器撒谎(例如from[property] as unknown as 12345
也会编译,但可能不是真的)。
与类型断言等效的是单{overload函数,您可以在其中提供所需的精确的调用签名,然后松开它以使实现不会抱怨:
// callers see this beautiful thing
function bar<T, K extends NumberFields<T>>(property: K): Adapter<T>;
// implementer sees this ugly thing
function bar(property: keyof any): Adapter<any>
{
return function (from: any): number {
return from[property];
}
}
另一种方法是尝试通过将函数重新键入为编译器可以理解的形式,以使编译器向您保证类型安全。这是一种不太那么嵌套的方法:
function bar<T extends Record<K, number>, K extends keyof any>(property: K): Adapter<T> {
{
return function (from: T): number {
return from[property]
}
}
}
在这种情况下,我们用T
来表示K
,而不是相反。而且这里没有错误。不使用此方法的唯一原因是,您可能希望错误显示在K
而不是T
上。也就是说,当人们用错误的参数调用bar()
时,您希望它抱怨他们的K
类型是错误的,而不是抱怨他们的T
类型是错误的。
通过执行以下操作,您可以同时获得两全其美:
function bar<T, K extends NumberFields<T>>(property: K): Adapter<T>;
function bar<T extends Record<K, number>, K extends keyof any>(property: K): Adapter<T> {
{
return function (from: T): number {
return from[property]
}
}
}
最后一件事...我不确定您打算如何致电bar()
。您是要自己指定类型参数吗?
bar<Foo, "bar">("bar");
还是您希望编译器进行推断?
bar("bar")?
如果是后者,那么您可能很不走运,因为除了可能的某些外部环境之外,没有其他地方可以推断T
。因此,您可能需要进一步研究。
好的,希望能有所帮助。祝你好运!