13

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?

3
  • 1
    let keys: string[] does remove the error but introduces another one, since not TS can't guarantee that user1[key] or user2[key] is valid. Commented Nov 1, 2019 at 8:52
  • @VLAZ Yes, let keys: string[] does introduce another issue, but that would be a run-time error (if the array happens to contain a non-existant field name as a value). What we're looking for is a compile-time check to avoid it. Commented Nov 1, 2019 at 9:41
  • This can be simplified to a single object - const obj = { a: 42, b: 'hello world' }; for (let i in obj) { obj[i] = obj[i]; } Commented Oct 22, 2022 at 0:31

2 Answers 2

8

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

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the response. Although not ideal, as never works. Also, could you explain the set method in the second example?
You appear to have forgotten to define set. Is it perhaps a generic utility function?
0

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.

4 Comments

This solution allows value to be of any relevant value type. I.E. it is possible to assign string type to id property. Interesting how TS fails for such simple use-case.
@Heniker: no, it doesn't. Have you actually tried this? If you use setField(user1, "id", "foobar"), then the Typescript compiler rejects "foobar" as a valid argument, with the error Argument of type 'string' is not assignable to parameter of type 'number'. Try adding that line to the playground linked in this answer.
I phrased that wrong. Here is a ts playground link that emphasizes what I meant. Notice how in the last for of statement we assign empty string to every key without error.
@Heniker: interesting; for the loop with a variable key and fixed value, the type system treats all possible types as a union again. It does raise an error when you replace '' with user1: Argument of type 'User' is not assignable to parameter of type 'string | number'.. I added a note on that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.