网站搜索

为什么 TypeScript 无法推断相同对象类型的赋值?


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

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

如果 a 和 b 是相同类型,访问相同的属性 prop... 难道这不应该是可能的吗?

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

那么,我这里漏掉了什么?

更新:由于我把示例简化得太多,答案是删除 as const 部分……但我认为这是由于我的环境,因为我在 tsconfig.json 中使用了 strict: true……

然后,如果我尝试在没有 as const 的情况下访问,我会得到这个 TS7053 错误:

元素隐式具有 'any' 类型,因为类型为 'string' 的表达式不能用于索引类型 'I'。

在类型 'I' 上未找到带有 'string' 类型参数的索引签名。

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

然后就是我出现原始错误的时候(这可以很容易地用 as any 修复,但我想避免那样做)

答案#1

总体而言,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] 只能接收可分配给 prop 可能命名的任意属性的值;因此赋值目标的类型被推断为 I['n'] & I['s'],简化为 string & number,然后为 never。
  • 所以就 TypeScript 而言,赋值 a[prop]=b[prop] 是将类型为 string | number 的值赋给类型为 never 的赋值目标。由于 string | number 不能赋值给 never,这是一个类型错误。

最简单的解决办法是使用像 b[prop] 这样的类型断言,也就是告诉 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,它不是联合类型,所以赋值目标没有交叉类型。

答案#2

这是我在不使用类型转换的情况下让它工作的一个方法:

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