Reputation: 388
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
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
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
}
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])
}
Upvotes: 8