为什么 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]})
