ts循环赋值断言也报错?

type PersonType = typeof person

function cloneObj(obj: PersonType) {
    const newObj = {} as PersonType
    for (const key of Object.keys(obj)) {
        // 报错 为啥会有never类型
        // 类型"string|number"不可分配给类型"never"。
        // 类型"string"不可分配给类型"never"。
        newObj[key as keyof PersonType] = obj[key as keyof PersonType]
    }
    return newObj
}

const person = {
    name: '曜',
    sex: '男',
    age: 16 // 如果属性值都是字符串就不报错
}
cloneObj(person)
阅读 3.4k
4 个回答

挺有意思的一个问题。

首先要指出,题主这种写法,在 TS 3.4 之前是不会报错的。而在 TS 3.5 之后引入了一项名为 Fixes to Unsound Writes to Indexed Access Types 的破坏性变更后,才会报错。

相关的讨论有很多,比如 #30769#33834

那么为什么 TS 要把这种写法视为一种错误呢?

其实如果你只是取值(即 Read),是没问题的:

let v = obj[key as keyof PersonType]; // ok

此时 v 的类型会推断为 string | number,也就是 PersonType所有属性的联合类型

问题出现在赋值(即 Write)上。

我们可以看出,其实 PersonType 上每个属性的类型,是确定的,要么是 string、要么是 number,并不真的存在一个属性的类型是 string | number。因此我们在尝试下面的写法时,才会得到错误,这是符合预期的:

person.name = 16;  // error
person.age = '曜'; // error

但如果你用了索引属性去赋值,那么就会出现:

let key = 'name';
person[key as keyof PersonType] = 16;
let key = 'age';
person[key as keyof PersonType] = '曜';

就打破了上面这种预期,也就是所谓的 Unsound Writes

因此 TS 3.5 之后引入了这项破坏性变化,当你尝试这么做的时候,会把类型收窄为所有属性的交叉类型,只有满足此交叉类型的,才能正确赋值。具体到本题中得到的交叉类型也就是 string & number,但很显然,没有任何类型 T 是能满足 T extends string & number 的,因此得到了 never,故而抛出题中的异常。


改法有很多种,但我只推荐用泛型。

除非你这个 cloneObj 专门针对 PersonType 编写的,否则我建议你这么写:

function cloneObj<T extends NonNullable<unknown>>(obj: T) {
    const newObj = {} as T;
    for (const key of Object.keys(obj)) {
        newObj[key as keyof T] = obj[key as keyof T];
    }
    return newObj;
}
type PersonType = typeof person;

function cloneObj<T extends PersonType>(obj: T): T {
    const newObj = {} as T;
    for (const key of Object.keys(obj) as Array<keyof T>) {
        newObj[key] = obj[key];
    }
    return newObj;
}

const person = {
    name: '曜',
    sex: '男',
    age: 16
};

const clonedPerson = cloneObj(person);

可以追踪下这个issue,编译器没法感知此时obj[key]newObj[key]是同一个类型,他们管这叫correlated union types

此时obj[ky]的类型为string | number, 而要给newObj[key]的值只能是string或者number, 满足它的类型只能是string & number类型,即never,所以提示才会出现无法把string|number赋值给never的提示。

你可以尝试去收缩类型:

const k = key as keyof PersonType
if (k === 'age'){
    newObj[k] = obj[k]
} else {
    newObj[k] = obj[k]
}
新手上路,请多包涵
logo
Microsoft
子站问答
访问
宣传栏