const getObjectKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
interface AB {
a: string;
b: number;
}
export function test(config: AB) {
Utils.getObjectKeys(config).reduce((memo, key) => {
**memo[key]** = config[key];
return memo;
}, {} as AB);
}
memo[key] - TS2322: Type 'string | number' is not assignable to type 'never'. Type 'string' is not assignable to type 'never'.
It's only working if i rewrite it like (memo as any)[key]. Why i can't use it like this in typescript? How to initialize accumulator memo to fix this problem?
The compiler unfortunately can't follow the logic of the single line memo[key] = config[key]
when key
is of a union type like keyof AB
. It doesn't analyze that line by paying attention to the identities of the variables; it just looks at the types. But the only reason we know memo[key] = config[key]
is okay is because the identical key
is used on both sides. If we had key1
and key2
, both of type keyof AB
, then the compiler's analysis would be more reasonable.
That analysis is implemented in microsoft/TypeScript#30769. Given memo[key1] = config[key2]
, the compiler knows that reading from config[key2]
where key2
is a union results in another union, string | number
. But assigning to memo[key1]
is possibly unsafe. After all, key1
and key2
might be different members of the union. The only way for such an assignment to be safe is if config[key2]
would be assignable to memo[key1]
no matter what key1
was. That would mean config[key2]
would have to be both a string
and a number
: that is, the intersection string & number
. But there is no value of that type; it's impossible. And so the compiler collapses that intersection to the impossible never
type. So the compiler would rightly complain that memo[key1] = config[key2]
is an error, since, without knowing what key1
is, you can't safely assign anything to it. Such analysis is generally useful and catches real errors.
Unfortunately, when you have key
instead of key1
and key2
, then the compiler is overly cautious, since it's worried about the impossible event that you're assigning a string
to a number
or vice versa.
So what can we do? From a comment from the implementer of ms/TS#30769, it is apparent that the way to make TypeScript follow the intended logic is to make key
's type a generic type K
constrained to a union instead of a union itself. That is, you make the reduce()
callback generic as follows:
function test(config: AB) {
getObjectKeys(config).reduce(<K extends keyof AB>(memo: AB, key: K) => {
memo[key] = config[key];
return memo;
}, {} as AB);
}
This works because both sides of the assignment are seen as the generic type AB[K]
. It turns out that this is actually unsafe in the same way as with a union one would be (you could make key1
and key2
of type K
, and make K
the full union keyof AB
, and it would be allowed). But, as said in that comment, TypeScript admits certain types of unsafe behavior, and assignments between identical generic indexed access types are allowed.