Having a Typescript variable of union type A
type A = {
b: true
x: number
} | {
b: false
x: string
}
declare const v: A
i can properly assign property x
to correct type, by checking for property b
value type with an if discriminant block to protect type A
consistency
if (v.b) { // v.x is number
// ok for compiler
v.x = 3
// compiler error as v.x should be number
v.x = ''
} else { // v.x is string
// compiler error as v.x should be string
v.x = 3
// ok for compiler
v.x = ''
}
outside discriminant block v.x
correctly appears to be number | string
however, compiler doesn't complain assigning x
to number | string
despite that would break type A
consistency
v.x = 3 // ok for compiler
v.x = '' // ok for compiler
Is there a way to force the compiler to reject this?
check it out on typescriptlang.org/play
Okay, so I think I've found the canonical GitHub issue about this: microsoft/TypeScript#14150, a suggestion that "unsafe type-incompatible assignments should not be allowed". It's still an open issue (as of 2019-09-13) marked as "awaiting more feedback", so if you think you have a compelling use case that's not already mentioned in there you might want to comment in there. I wouldn't hold my breath waiting for this to be implemented, though, since the related issues like enforcing readonly strictness via flag flag and enabling variance annotations are either closed or haven't been acted upon.
The problem here involves type system's lack of soundness. A sound type system would only let you do safe things. But here it lets you make a property assignment to an object that might violate the object's declared type. This unsafe permissiveness means the type system is unsound. That, by itself, is not considered a bug. It is not one of TypeScript's design goals to "apply a sound or 'provably correct' type system". There is a tradeoff between correctness and productivity, and it is quite possible that fixing this issue might be more trouble than it's worth. See microsoft/TypeScript#9825 for more discussion about TypeScript's soundness and/or lack thereof.
The particular unsoundness here: the compiler assumes that it is safe to write the same type to a property that you can read from it. This is not true in general, as shown in your example, and in this related example from the linked issue:
interface A { kind: "A"; foo(): void; }
interface B { kind: "B"; bar(): void; }
function setKindToB(x: A | B): void {
x.kind = "B"; // clearly unsafe
}
So what can be done? Not sure. TypeScript 3.5 introduced a change to indexed access writes (such as foo[bar] = baz
) so that if the key is of a union type (say bar
is Math.random()<0.5 ? "a" : "b"
) then you must write the intersection of the property types to it, not the union (so the type of baz
must be typeof foo.a & typeof foo.b
and will no longer accept typeof foo.a | typeof foo.b
). This is a soundness improvement that prohibits some invalid things which were previously allowed. And it also prohibits lots of valid things which were previously allowed. And lots of people are still upset about it and new issues about it are still filed fairly frequently. I imagine the same problem would happen here if they fixed this issue... you would get the error you expect, and lots of code bases would break. For now I'd say you should probably just avoid doing these assignments, which I understand is not much consolation.
Anyway, hope that information is of some use to you. Good luck!