Minimal example of the problem:
const a = {
'x': {},
'y': 'y',
};
const b = {
'x': {},
'y': 'y',
};
for (const p of ['x', 'y'] as const) {
a[p] = b[p]; // error string | {} is not assignable to {} & string
}
for (const p of ['x', 'y'] as const) {
switch (p) {
case 'x':
a[p] = b[p]; // no error
break;
case 'y':
a[p] = b[p]; // no error
break;
}
}
Typescript sees the type of p
as 'x' | 'y'
but doesn’t correlate the fact that a
and b
are being indexed by the same value.
Is there a way to… distribute? the union operation so that essentially the typed expression becomes goes from a['x'|'y'] = b['x'|'y']
to a['x'] = b['x'] | a['y'] = b['y']
I’ve tried generic functions and a host of attempts at coercing the types of the inputs but short of asserting as any
nothing seems to work.
I’d like to avoid type as
assertions (especially to any
) as it adds one more area where type analysis is likely to fail, but I feel like I don’t have any other options other than generating code during build but that’s a rather large additional build step I’d rather not add for something that feels small like this.
A switch statement isn’t a feasible alternative since the actual number of keys is much larger than 2 and is liable to change so this would involve adding many sections of duplicated code throughout my codebase.
This is a consequence of a type safety improvement to indexed access types introduced in TypeScript 3.5, as implemented by microsoft/TypeScript#30769. In general it is unsafe to allow a[p] = b[q]
where a
and b
are of the same object type and where p
and q
are of the same union type, since if p
and q
turn out to be different elements of the union, you might be writing an incompatible value. In your case you are doing a[p] = b[p]
, but from the type system’s perspective that’s the same thing; all it sees is that you are writing and reading an object property whose key is a union type "x" | "y"
, which is unsafe in general. It doesn’t pay attention to the fact that if you’re using the exact same value p
in both places, then it has to be safe.
So since TypeScript 3.5 this has been a pain point. There is a request to fix this for when you are reading or writing the “same” property; see microsoft/TypeScript#32693. And, fortunately, according to this comment it looks like this will be fixed for the case here where you’re literally using the same identifier (like p
) as the key. Not sure when that will happen, though… the issue seems to be on the Backlog and not slated for a particular release of TypeScript. So it could be a while.
Until then it should be possible to refactor to a generic function, since one place they still allow the older pre-TS-3.5 unsafe access is when you are using generic type. This is mentioned in a comment on #30769:
One rule we’ve always had is that any given type (and, by extension, any
T[K]
for identicalT
andK
) is by definition assignable to itself, and that’s the basic unsoundness we permit
So if we introduce this indirection:
function copyProp<T, K extends keyof T>(dst: T, src: T, key: K) {
dst[key] = src[key];
}
That compiles just fine, and now we can use it:
for (const p of ['x', 'y'] as const) {
copyProp(a, b, p);
}
which also compiles without error. It’s annoying, but at least there is a solution/workaround that works for now, at least until a fix for #32693 is released.
One last thought about wishing this could be fixed in general so you could avoid switch statements. A while ago I opened a feature request microsoft/TypeScript#25051 to allow for “opt-in distributive control flow analysis” where you could say something like type switch (p) {...}
and have the compiler evaluate the enclosed code block once for each element of the union type of p
, and if each pass succeeded, then the whole thing would succeed. The compiler feasibly can’t do that kind of multi-pass analysis for each union-typed expression it encounters, but I was hoping we could at least have some syntax to ask for it in specific cases. Alas, it is not to be (and was closed as a duplicate of one of the several issues it would address), but when I see this issue I become wistful and think of what might have been…. Sigh…
Okay, hope that helps; good luck!
Playground link to code
1