打字稿:按泛型类型的窄键索引时的对象属性类型



我需要提供一个适配器函数,该函数接受一个对象并返回一个或多个给定类型的值。 我正在尝试编写一个通用函数,该函数将在给定对象属性名称的情况下生成这样的适配器函数,其中属性名称仅限于其值为给定类型的属性。

例如,考虑数值属性的简单情况:

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 问题,其中缩小 TypeScript 索引类型似乎最接近我想要的,但类型保护在这里没有用,因为类型应该是泛型的。

编译器不够聪明,无法看穿泛型类型参数的这种类型操作。 对于具体类型,编译器可以将最终类型评估为一袋具体属性,因此它知道输出将是类型Foo['bar']即类型number

这里有不同的方法可以继续。 通常最方便的是使用类型断言,因为您比编译器更聪明。 这使发出的 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也会编译器,但可能不是真的)。

等效于类型断言的是单重载函数,您可以在其中给出一个根据需要精确的调用签名,然后将其松开,以便实现不会抱怨:

// 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] 
}
}
}

在这种情况下,我们用K来表示T,而不是相反。这里没有错误。 不使用此方法的唯一原因是,也许您希望错误出现在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。 因此,您可能想对此进行更多研究。


好的,希望有帮助。 祝你好运!

最新更新