Vaibhav K
Vaibhav K

Reputation: 388

How to avoid dynamic keyof object assign error in TypeScript

Let's say we have TypeScript code that looks like:

type User = {
  id: number,
  name: string,
}

let user1: User = {id: 123, name: "Hello"};
let user2: User = {id: 456, name: "World"};

let keys: (keyof User)[] = ["id", "name"];

for (let key of keys) {
  user1[key] = user2[key];
}

This gives error

Type 'string | number' is not assignable to type 'never'.

for the statement

user1[key] = user2[key];

If we change the definition of keys to

let keys: string[] = ["id", "name"];

the error goes away, but we lose type safety.

Is there some way we can avoid this error while still maintain type safety?

Upvotes: 13

Views: 1239

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1121554

You'll have to use a utility function, to help TypeScript trust that the value is a specific, single type from the union of value types:

function setField<T, K extends keyof T>(o: T, key: K, value: T[K]) {
  o[key] = value
}

for (let key of keys) {
  setField(user1, key, user2[key])
}

This tells the compiler that if the second argument for the function is a valid key for the type of the first, then the third argument must be a valid value for that key. If the first argument is a User instance, and the second is a valid key, then the last argument will be checked against typeof User[key], and otherwise you'll get an error.

I've created a playground demo for you to play with.

Without the function, user2[key] is resolved to be the intersection of all possible types in the User interface values, and since an object can't be a number and a string at the same time, you end up with never. And never is not a valid type for any of the fields in User (or for anything else in TS, for that matter).

Also see the Typescript 2.1 section on lookup types.

Note: the type system will not protect you from using a fixed value with a variable key, e.g. for (let key of keys) setField(user1, key, 'string') passes because the union of all acceptable values across all the keys includes both string and number.

Upvotes: 0

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249536

There is no good way to avoid a type assertion here. In recent version on TS (post 3.5 I think) when writing through an index the value written has to be compatible with all possible property values specified by the key. In your case that would be number & string which reduces to never hence the error.

The root cause is that TS does not keep track of variables only of types, so as far as the types are concerned, your example would be no different from:

let key1 = 'id' as  keyof User;
let key2 = 'name' as  keyof User;
//Obvious error
user1[key1] = user2[key2] // same error, TS can't distingusih between this and your user1[key] = user2[key]

The simplest solution is to use a type assertion if, as in your case you are sure this is ok :

type User = {
  id: number,
  name: string,
}


let user1: User = { id: 123, name: "Hello" };
let user2: User = { id: 456, name: "World" };
for (let key of keys) {
  user1[key] = user2[key] as never
}

Play

Alternatively (but not any more type safe) you can use a small loophole where T[K] is assignable to index value:

type User = {
  id: number,
  name: string,
}


let user1: User = { id: 123, name: "Hello" };
let user2: User = { id: 456, name: "World" };

let keys: (keyof User)[] = ["id", "name"];

for (let key of keys) {
  set(user1, key, user2[key])
}

Play

Upvotes: 8

Related Questions