如何在打字稿中设置对象分配时的键类型和值类型

问题描述 投票:0回答:4
interface Student {
  id: number
  name: string
  age: number
}
let alice: Student = { name: "alice",  age: 19, id: 1 }
const bob: Student = { name: "Bob", age: 19, id: 2 }

const keys: (keyof Student)[] = ["age", "name"]
for (let c of keys) {
  alice[c] = bob[c]  // here shows the error during compile
  /*
  src/index.ts:11:3 - error TS2322: Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.
  */
}

如何编辑代码才不会出现打字错误

alice[c] = bob[c]

typescript typescript-generics
4个回答
2
投票

目前,当您编写

alice[c] = bob[c]
形式的代码时,编译器仅分析键的类型,而不分析它们的身份。这意味着它无法区分
alice[c] = bob[c]
alice[c1] = bob[c2]
之间的区别,其中
c1
c2
c
都是同一类型。 人们可能会认为这很好,并且分配的双方都是索引访问类型
Student[keyof Student]

但是当键是像keyof Student这样的联合类型时,TypeScript 会以不同的方式对待索引访问

写入
。这个安全功能是在 microsoft/TypeScript#30769 中实现的,它确实可以捕获合法的问题,因为
alice[c1] = alice[c2]
确实安全;例如,如果
c1
"id"
c2
"name"
会怎么样?

但它也禁止一些完全安全的代码。 对于

alice[c] = bob[c]
,我们知道每一侧的
c
不仅仅是相同的类型,而且是相同的值。 在 microsoft/TypeScript#32693 有一个支持这种“同键”分配的功能请求,但尚未实现。


除非实现,否则您需要重构到编译器将赋值的两侧视为相同或兼容的版本。 根据 对 microsoft/TypeScript#30769 的评论,如果

k
generic 类型,你可以得到这个。

因此,这里最简单的方法是将赋值放在泛型函数中...而最简单的方法是从 for...of 循环

 更改为 
forEach() 数组方法
,其中接受回调:
keys.forEach(<K extends keyof Student>(c: K) => { alice[c] = bob[c] });

现在 
c

是泛型类型

K
,赋值的两边都被视为泛型索引访问类型
Student[K]
,这是允许的。

Playground 代码链接


2
投票
c

循环中的

for
具有类型
keyof Student
,即可以是
Student
的任意键,因此
alice[c]
bob[c]
的类型是
Student中所有属性类型
的并集,即string | number
问题是,当你尝试做作业
alice[c] = bob[c]

时,TS不够聪明,无法知道

alice[c]

bob[c]
必须是同一类型:TS只知道
string | number
,然后右侧的类型,但在左侧变为(等于)
string & number
(并集变为交集!),进而简化为 
never
。  错误从何而来。
这是有效的代码,不一定适合您,具体取决于您实际需要的“通用”程度,但它说明了这样一个事实:如果所有属性都具有相同的类型,则分配会很好地工作,因此,类型保护使得在每个条件分支中都有一个已知的唯一属性类型足以完成这个任务:

interface Student { id: number name: string age: number } let alice: Student = { name: "alice", age: 19, id: 1 }; const bob: Student = { name: "Bob", age: 19, id: 2 }; declare const keys: (keyof Student)[]; // to show that results do not depend on the specific value of `keys` for (const c of keys) { if (c === "name") { c; // const c: "name" alice[c] = bob[c]; // no error } else { c; // const c: "id" | "age" alice[c] = bob[c]; // no error } c; // const c: keyof Student const a_ = alice[c]; // const a_: string | number const b_ = bob[c]; // const b_: string | number alice[c] = bob[c]; // ERROR // Type 'string | number' is not assignable to type 'never'. } type TEST = string & number; // type TEST = never

游乐场链接

我认为帮助 TypeScript 检测带有

2
投票
(

keyof Student) 的 索引访问

 目标与 
Student
 相同的属性的最简单的调整是添加一个 
generic
 来推断确切的 
keyof Student
不要对语法感到困惑。它只是一个包含作业的 
IIFE

interface Student { id: number; name: string; age: number; } const alice: Student = { name: "alice", age: 19, id: 1 }; const bob: Student = { name: "Bob", age: 19, id: 2 }; const keys: (keyof Student)[] = ["age", "name"]; for (let k of keys) { (<K extends keyof Student>(k: K) => { alice[k] = bob[k]; })(k); }

TypeScript 游乐场

如果您可以仅使用

1
投票
中所需的键创建对象,则只需使用

Object.assign()
将它们复制到
alice
。这个问题回答了如何创建仅包含键子集的正确类型的对象:
如何获取 javascript 对象属性的子集

我稍微简化了该问题中pick()

的实现,以获得以下简单干净的解决方案:

const pick = <T extends {}, K extends keyof T>(obj: T, ...keys: K[]) => 
  Object.fromEntries(keys.map(key => [key, obj[key]])) as Pick<T, K>;

interface Student {
    id: number
    name: string
    age: number
}

let alice: Student = { name: "alice", age: 19, id: 1 };
const bob: Student = { name: "Bob", age: 19, id: 2 };

const keys: (keyof Student)[] = ["age", "name"];

Object.assign(alice, pick(bob, ...keys));

游乐场链接

© www.soinside.com 2019 - 2024. All rights reserved.