type Type = {
aa: string;
bb: number;
};
const keys = ['aa', 'bb'] as (keyof Type)[];
const target = {} as {
[Key in keyof Type]: Type[Key];
};
const source: Type = {
aa: 'aa',
bb: 123
};
keys.forEach((key) => {
const s = source[key]; //string | number
const t = target[key]; //string | number
/**
* Error: Type 'string | number' is not assignable to type 'never'.
* Type 'string' is not assignable to type 'never'.
*/
target[key] = source[key];
});
As the code show above target[key] = source[key];
is not allowed, here is my reason about:
source[key]
is string | number
and target[key]
is also string | number
, so the assignment is not allowed, but the compiler ignore the fact that key
is actually the same one, i.e. if source[key]
is string
, target[key]
must be string as well,
How do I tell the typescript about the constraint, it seem that I should put some generic type param somewhere, but no idea what exactly I should do
CodePudding user response:
As you note, the following doesn't work
keys.forEach(key =>
target[key] = source[key] // error! string | number not assignable to never
);
because the compiler is not keeping track of the correlation between the union types of target[key]
and source[key]
. They are both string | number
, but the compiler does not realize that they will either both be string
or both be number
. It does not notice that the identity of key
ties the two sides of the assignment together.
Since indexed access types were made more strict in TypeScript 3.5, as implemented in microsoft/TypeScript#30769, the compiler will only allow assignments like target[key] = source[key]
for union-typed key
if every possible assignment would succeed, by treating the assignment target as the intersection of the property types. That is, target[key]
would only accept a value of type string & number
(a.k.a. the impossible never
type) instead of string | number
.
Lack of direct support for correlated unions is reported at microsoft/TypeScript#30581.
And the recommended fix, as described in microsoft/TypeScript#47109, is to use generics. Essentially, when key
is generic and when target
and source
have the same type, then the compiler will allow target[key] = source[key]
(even though this is unsound, as mentioned in this comment in microsoft/TypeScript#30769).
So to fix your example, we should make the forEach()
callback a generic function, like this:
keys.forEach(<K extends keyof Type>(key: K) => {
target[key] = source[key]; // okay
});
Now key
is of generic type K
constrained to keyof Type
. Both sides of the assignment are of identical generic type Type[K]
, and the compiler allows this.