Given this code in Typescript:
interface InterfaceA {
transfer?: string[];
deposit?: number[];
test?: boolean[];
}
interface InterfaceB extends InterfaceA {
moretest?: boolean[];
}
type Label = ("transfer" | "deposit" | "test")
const a: InterfaceA = {}
const b: InterfaceB = {}
const labels: Label[] = ["transfer" , "deposit" , "test"];
for (const label of labels){
a[label] = b[label]
}
You get the error:
Type 'string' is not assignable to type 'number'
It fails to compile because a[label]
(left side of the assignment) can be one of string[]
, number[]
or test[]
while b[label]
(right side of the assignment) can also be one of string[]
, number[]
or test[]
.
It kinds of make sense, except that Typescript should know that if the left side is string[]
, the right side will be string[]
too, same applies for the other types.
This is why replacing the assignment with a[label as "transfer"] = b[label as "transfer"]
works.
Now, what should I change in order to make it work without having to hardcode the typecasted value?
Other viable options which I don't like:
a[label] = b[label] as any
if (label == "transfer") {
a[label] = b[label]
} else if (label == "deposit") {
a[label] = b[label]
} else {
a[label] = b[label]
}
TypeScript can't handle "correlated union" types in which a single block of code is checked multiple types for each member of a union. This issue is described in detail in microsoft/TypeScript#30581.
From a pure type system perspective, it's unsafe, because the type of b[label]
is the InterfaceB[Label]
union, and the compiler is worried that when you try to assign it to a[label]
of union type InterfaceA[Label]
, maybe the two keys are actually different? That is, it's ignoring the identity of label
and just looking at the type. Put another way: imagine you had label1
and label2
both of type Label
. Then a[label1] = b[label2]
would clearly be unsafe. (See microsoft/TypeScript#30769 for the pull request implementing this safety check.) Of course you're not doing this, but the compiler is unable to see the difference because the types are the same for both situations.
Of course you could fix it by writing a[label] = b[label]
once for each narrowing of label
, but that's not what you want to do. You want a single block of code to be checked for multiple cases.
If you need a single block of code to be checked for multiple cases, you'll have to refactor to use generics instead of unions. This can often be tricky, and in general requires some significant changes to the types involved, as described in microsoft/TypeScript#47109. In your case, though, it's easy enough to iterate the array and use a generic callback function, like this:
labels.forEach(<K extends Label>(label: K) => {
a[label] = b[label];
});
That works because the label is generic K
, and the compiler is able to relate InterfaceA[K]
to InterfaceB[K]
in a way that it can't for InterfaceA[Label]
and InterfaceB[Label]
.
That code uses the array forEach()
method because it's much easier to use a callback there. You could have done it in a loop like
const cb = <K extends Label>(label: K) => { a[label] = b[label] };
for (const label of labels) {
cb(label);
}
or even
for (const label of labels) {
(<K extends Label>(label: K) => { a[label] = b[label] })(label);
}
but forEach()
is more idiomatic.
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With