Typescript:通过通用类型的缩小键索引的对象属性类型

时间:2019-03-26 10:20:59

标签: typescript generics return-type

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

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

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

1 个答案:

答案 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。因此,您可能需要进一步研究。


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