【问题标题】:Typescript: object property type when indexed by narrowed keyof generic type打字稿:由泛型类型的窄键索引时的对象属性类型
【发布时间】:2019-03-26 10:20:59
【问题描述】:

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

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

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

【问题讨论】:

    标签: typescript generics return-type


    【解决方案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 也会编译但可能不是真的)。​​

    与类型断言等效的是 single-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] 
        }
      }
    }
    

    在这种情况下,我们用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。因此,您可能需要对此进行更多研究。


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

    【讨论】:

    • 感谢您提供的非常有帮助的答案!我目前正在使用您的第一个建议 - 转换为 any 然后返回输出类型 - 但肯定会调查您的第三个建议。我在研究过程中遇到过类似的事情,但出于您给出的原因将其排除在外。我没有考虑过重载来解决这个问题。天才!回答你的问题:是的,我希望函数调用是显式类型的,尽管在我的实际实现中我只使用一个泛型参数并在参数上使用缩小类型,a la foo,所以只需要给出 T。再次感谢!
    猜你喜欢
    • 2021-12-06
    • 2021-12-30
    • 2019-03-12
    • 2019-01-16
    • 2020-10-11
    • 2017-09-15
    • 2020-05-25
    • 1970-01-01
    • 2021-11-26
    相关资源
    最近更新 更多