【问题标题】:Type '1' is not assignable to type 'T[Extract<keyof T, string>]'类型 '1' 不可分配给类型 'T[Extract<keyof T, string>]'
【发布时间】:2019-11-17 10:24:39
【问题描述】:

我有一个下面的函数,它接受一个 T 类型的参数,它扩展了 Foo(它是一个对象)。在函数中,它遍历给定对象的每个键以创建一个具有完全相同键但对应值全为 1 的新对象(此函数的作用并不重要)。

但是使用Type '1' is not assignable to type 'T[Extract&lt;keyof T, string&gt;]'. 编译失败。我认为T[Extract&lt;keyof T, string&gt;]number,所以分配1number 应该可以工作。

我的代码有什么问题?

type Foo = {
  [key: string]: number
}

const func = <T extends Foo>(obj: T): T => {
  for (const name in obj) {
    obj[name] = 1
  }
  return obj
} 

【问题讨论】:

    标签: typescript typescript-typings typescript-generics


    【解决方案1】:

    编译器通常不会对泛型类型的操作进行非常复杂的分析(即,类型依赖于未解析的类型参数,如 Tfunc() 的实现中)......它往往更擅长于处理更直接的具体类型(如Foo)。

    所以编译器非常满意,并允许您的函数的以下具体版本:

    const concreteFunc = (obj: Foo): Foo => {
      for (const name in obj) {
        obj[name] = 1; // okay
      }
      return obj; // okay
    };
    

    由于尚不知道未解析的泛型类型,编译器将不太确定您正在执行的操作是否安全,并且可能会发出警告。此警告并不一定意味着您肯定犯了错误。

    这种情况经常发生在泛型函数的实现中。如果你仔细分析你在做什么并确定它确实是类型安全的,你可以使用type assertions 来删除警告。

    例如,您可以这样做:

    const func = <T extends Foo>(obj: T): T => {
      for (const name in obj) {
        obj[name] = 1 as T[typeof name]; // assert BUT BEWARE ☠
      }
      return obj;
    };
    

    但请注意,类型断言意味着类型安全的责任已从编译器转移到您身上……并且(回答您的问题)这是不安全的

    这就是为什么...考虑以下代码:

    interface Bar extends Foo {
      two: 2;
      four: 4;
      six: 6;
      eight: 8;
    }
    
    const bar: Bar = {
      two: 2,
      four: 4,
      six: 6,
      eight: 8
    };
    
    const b = func(bar);
    
    console.log(b.two); // 2 at compile time, but prints 1!
    console.log(b.four); // 4 at compile time, but prints 1!
    console.log(b.six); // 4 at compile time, but prints 1!
    console.log(b.eight); // 4 at compile time, but prints 1!
    

    这里我们看到一个接口Bar,它通过添加值为numeric literals的已知属性扩展Foo,其中没有一个等于1。当我们调用func(bar)时,T被推断为Bar,因此func(bar)的输出也应该是Bar

    坏事发生了。我们有一个对象,其已知属性在编译时应该是偶数,但在运行时实际上是数字1

    这就是为什么你可能不应该在像func() 这样的函数中使用断言。写func()... 可能有一种实际上安全的方法,可能是这样的:

    const funcSafer = <
      T extends { [K in keyof T]: 1 extends T[K] ? unknown : never }
    >(
      obj: T
    ): T => {
      for (const name in obj) {
        obj[name] = 1 // error! still need "as T[typeof name]"
      }
      return obj;
    };
    

    这里,T 的约束特别是 1 应该可以分配给它的所有属性。这具有以下理想效果:

    funcSafer(bar); // error! property "two" is incompatible
    const foo: Foo = {two: 2, four: 4}; // just Foo, not Bar
    funcSafer(foo); // okay
    funcSafer({a: 1 as 1}); // okay
    funcSafer({a: 4}); // okay, interpreted as {a: number}
    funcSafer({a: 4 as 4}); // error, "a" is incompatible
    

    当然,编译器仍然无法判断obj[name] = 1 在实现中是安全的。这太复杂了……所以我们需要断言。

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

    Link to code

    【讨论】:

    • 非常感谢您的详细解释!我想知道为什么知道obj[name]number 类型很复杂。看起来很简单。
    • 我们知道obj[name] 可以分配给number,但我们确实知道number 可以分配给obj[name]。所以const x: number = obj[name] 可以,但obj[name] = 1234 不行,因为obj[name] 的类型可能比数字窄。这就是Bar 示例所显示的内容。 bar.two 的类型是 2,而不是 number。您可以安全地将2 类型的值复制到number 类型的变量中,但是您不能安全地将number 类型的值复制到2 类型的变量中。这有意义吗?
    • > obj[name] 比数字窄啊,我明白了 :) 谢谢!
    • @jcalz 那么,类型断言是目前唯一的解决方法吗?我知道在这种情况下,我比编译器更了解参数的形状,但它至少不应该能够推断出如果泛型被限制为,例如,{ [ P in keyof T ] : number },那么值类型 number 可分配给 T[P]。是否有充分的理由说明情况并非如此?
    • 原因与这里多次提到的相同:T[P] 可分配给number,但这并不意味着number 可分配给T[P]。数值文字类型或它们的联合是此类类型的典型示例。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-11-10
    • 2021-11-16
    • 2016-07-31
    • 2019-01-08
    • 2022-11-11
    • 2022-11-11
    • 2017-12-29
    相关资源
    最近更新 更多