【问题标题】:Why TypeScript can't infer assignments of the same object type?为什么 TypeScript 不能推断出相同对象类型的赋值?
【发布时间】:2020-05-19 08:00:42
【问题描述】:

假设我有一个非常简化的示例来说明我的代码中发生的事情:

interface I {
  n?: number;
  s?: string;
}

const a: I = {
  n: 1,
}

const b: I = {
  n: 2,
  s: 'b',
}

const props = ['n', 's'] as const;

for (const prop of props) {
  if (!a[prop]) {
    // here, I get the following error:
    // TS2322: Type 'string | number' is not assignable to type 'never'.
    //   Type 'string' is not assignable to type 'never'.
    a[prop] = b[prop];
  }
}

作为ab 相同的类型,访问相同的属性prop... 不应该是可能的吗?

  • 如果b[prop] 是一个数字,那么a[prop] 应该接受数字
  • 如果b[prop] 是一个字符串,那么a[prop] 应该接受字符串
  • 如果b[prop] 未定义,是因为该属性是可选的?

那么,我在这里错过了什么?

更新:由于我过于简化了我的示例,答案是删除了as const 部分......但我认为这是由于我的环境,因为我使用的是strict: truetsconfig.json...

然后,如果我尝试在不使用 as const 的情况下访问,我会收到 TS7053 错误:

元素隐式具有“任何”类型,因为“字符串”类型的表达式不能用于索引类型“I”。

在“I”类型上找不到带有“字符串”类型参数的索引签名。

修复添加as const 或执行for(const prop of props as ('n' | 's')[]) {

然后是我收到原始错误的时候(可以使用as any 轻松修复,但我正在努力避免这种情况)

【问题讨论】:

    标签: typescript


    【解决方案1】:

    问题出在这一行

    const props = ['n', 's'] as const;

    改成

    const props = ['n', 's'];

    检查这个stackblitz

    【讨论】:

    • 真的,行得通。背后推理?无论如何,这是我的错,因为在我的真实示例中,这个解决方案不起作用......让我更新问题:)
    • 所以打字稿接受的数据的真实类型是模糊的?看来是套路了。 'n' | 's' 理论上应该可以工作。你能解释一下为什么不应该吗?
    • 为什么要投反对票?
    【解决方案2】:

    我试过这样和它的工作,你应该删除 props 定义中的 const 值

    interface I {
      n?: number;
      s?: string;
    }
    
    const a: I = {
      n: 1,
    }
    
    const b: I = {
      n: 2,
      s: 'b',
    }
    
    const props = ['n', 's'];
    
    for (const prop of props) {
      if (!a[prop]) {
        a[prop] = b[prop];
        console.log(a, b);
      }
    }
    

    结果是: {n: 1, s: "b"} {n: 2, s: "b"}

    【讨论】:

      【解决方案3】:

      一般来说,Typescript 不会证明代码的所有可证明属性,也不会尝试证明。在这种特殊情况下,我们人类知道 b[prop] 的类型与 a[prop] 的类型匹配,因为属性名称是相同的,但是 Typescript 看不到这一点,因为它没有允许它的规则.

      如果我们遵循 Typescript 的逻辑:

      • props 被声明为 ['n', 's'] 类型的元组。
      • prop 被声明为props 的一个元素,因此其类型被推断为'n' | 's'
      • 由于b 被声明为I 类型,因此b[prop] 被推断为I['n' | 's'] 类型,简化为string | number
      • 由于a 被声明为I 类型,那么赋值目标 a[prop] 只能接收一个可赋值给@987654337 的任何 属性的值@ 可能命名;因此分配目标的类型被推断为I['n'] & I['s'],简化为string & number,然后是never
      • 就 Typescript 而言,赋值 a[prop] = b[prop] 是将 string | number 类型的值赋给 never 类型的赋值目标。由于string | number 不能分配给never,这是一个类型错误。

      最简单的解决方法是使用像 b[prop] as any 这样的类型断言,即告诉 Typescript 你知道你的代码是类型安全的,因此不需要检查它。另一种选择是使用辅助函数,它以满足 Typescript 类型检查器的方式执行分配:

      function copyMissingProperties<T>(a: T, b: T, props: readonly (keyof T)[]): void {
          for (const prop of props) {
              if (!a[prop]) {
                  a[prop] = b[prop]; // ok
              }
          }
      }
      

      这里prop的类型是keyof T,不是联合类型,所以赋值目标没有交集类型。

      【讨论】:

      • 很好的解释。是的,我可以使用any joker 卡让它轻松工作......但我试图避免它,这就是我问的原因:) - 但我会尝试更新一个更真实的例子的问题
      • 是的,any 只是最简单的方法; copyProperty 函数无需任何类型断言即可工作。
      • ...我在回答中展示了两个选项。 copyProperty 函数不使用any 或任何其他类型的断言。至于“通知赋值正确的正确方法”,你的意思似乎是告诉编译器不要检查赋值中的类型,因为你知道它是安全的。这正是类型断言的预期目的,任何替代语法都将具有与类型断言完全相同的缺点,因为它在语义上是相同的——它会告诉编译器不要检查代码的某个部分。跨度>
      • 如果有一种方法可以让编译器执行我称之为distributive control flow analysis 的操作,它会遍历每个可能的缩小范围,那就太好了。你会说“请对分配进行多次传递,就好像 prop 在每次传递中是 "n" | "s" 的不同元素一样,并确保它始终键入检查”。它不会被实现,但如果没有泛型或外部函数,它将是类型安全的。现在我们所能做的就是断言或使用泛型。 ?‍♂️
      • @danikaze 您可以在一个函数中完成所有分配,这可能看起来更自然且性能更高。我已经编辑了示例代码来做到这一点;它进行类型检查的原因与执行一项分配的原因相同。
      【解决方案4】:

      这是我在不使用强制转换的情况下让它工作的一种方法:

      interface I {
          n?: number;
          s?: string;
      }
      
      const a: I = {
          n: 1,
      }
      
      const b: I = {
          n: 2,
          s: 'b',
      }
      
      const props: Array<keyof I> = ['n', 's'];
      
      for (const prop of props) {
          if (!a[prop]) {
              const newProp: I = {[prop]: b[prop]};
              Object.assign(a, newProp);
          }
      }
      

      诀窍是使用 Object.assign。但是,为了仍然确保类型安全,您可以将新属性声明为 I 类型的对象,然后在 Object.assign 中使用它。为了在所有情况下都能正常工作,最好的方法是只做Object.assign(a, {[prop]: b[prop]})

      【讨论】:

      • 这在道德上等同于类型断言(TS 具有类型断言;最好避免使用“cast”一词,因为它具有运行时强制的含义,而 TS 类型断言绝对不会发生这种情况),从某种意义上说,Object.assign(a, {craziness: {foo: "bar"}}); 也将“工作”。
      • @jcalz,查看最后的编辑,这应该可以缓解此类担忧
      • 那肯定更好。这里仍然有可能出现误报,从这个故意荒谬的示例中可以看出:const newProp: I = { [Math.random() &lt; 0.5 ? "dog" : "cat"]: b[Math.random() &lt; 0.5 ? "n" : "s"] };(计算出的[prop] 键被映射到一个字符串索引签名,该签名可以不安全地缩小回keyof I,并且分配的值不一定保证是正确的类型)。我并不是要阻止这种方法,而是要警告我看到人们陷入的陷阱。祝你好运!
      猜你喜欢
      • 1970-01-01
      • 2011-12-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-29
      • 2021-04-25
      • 2018-06-04
      • 2012-01-15
      相关资源
      最近更新 更多