typescripttypescript-generics

How to set the key type and value type during object assign in typescript


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'.
  */
}

How to edit the code so that not typscript error will occur during alice[c] = bob[c]?


Solution

  • Currently when you write code of the form alice[c] = bob[c], the compiler only analyzes the types of the keys and not their identities. That means it can't tell the difference between alice[c] = bob[c] and alice[c1] = bob[c2] where c1 and c2 are both the same type as c. One might think this is fine and that both sides of the assignment are of the indexed access type Student[keyof Student].

    But TypeScript treats indexed access writes differently when the key is a union type like keyof Student. This safety feature was implemented in microsoft/TypeScript#30769, and it does catch legitimate problems, since alice[c1] = alice[c2] really isn't safe; what if c1 is "id" but c2 is "name", for example?

    But it also prohibits some perfectly safe code. For alice[c] = bob[c] we know that the c on each side isn't just of the same type, it's the identical value. There's a feature request to support this sort some "same-key" assignment at microsoft/TypeScript#32693, but it hasn't been implemented yet.


    Until and unless it is implemented, you'll need to refactor to a version where the compiler sees the two sides of the assignment as identical or compatible. According to a comment on microsoft/TypeScript#30769 you can get this if k is of a generic type.

    Therefore the easiest approach here is to put the assignment inside a generic function... and the easiest way to do that is probably to change from a for...of loop to the forEach() array method, which takes a callback:

    keys.forEach(<K extends keyof Student>(c: K) => {
      alice[c] = bob[c]
    });
    

    Now c is of generic type K, and both sides of the assignment are seen as the generic indexed access type Student[K], which is allowed.

    Playground link to code