It appears to me that Typescript is collapsing a wrapped union type (undesired) when it happens to know the initial value. I'm not sure that this is a bug, but I'm wondering if there's a way to avoid it.
An example:
type Prime = 3|5|7;
type OrDonuts<T> = T | 'donuts';
function compare<T>(a: T, b: OrDonuts<T>) {
return a == b;
}
let value: Prime;
compare(value, 3);
// OK!
value = 5;
compare(value, 3);
// TS2345: Argument of type '5' is not assignable to parameter of type 'OrDonuts<3>'
In order to get around this error, I have to explicitly uncollapse by saying things like value = 5 as Prime;
.
Is this a bug, expected behavior, am I just doing it wrong?
(node: 10.15, typescript: 3.5.1)
The TypeScript type checker performs control flow type analysis, meaning that it makes an effort to determine what happens to the values inside variables at runtime and will narrow the types of these variables to match. One of the specific ways this happens is if you have a variable or property of a union type, and the compiler sees you assign a more specifically-typed value to it, it will narrow the type of the variable to that more specific type, at least until you assign some different value to the variable.
This is very often desirable, since it supports use cases like this:
let x: number | string = Math.random() < 0.5 ? "hello" : "goodbye"; // x is string now
if (x.length < 6) { // no error here
x = 0; // the widest x can be is string | number, so this assignment is fine
}
Note that even though you have annotated that x
is a number | string
, the compiler understands that it will definitely be a string
after the initial assignment. So it does not complain when you check x.length
(as it would if x
could possibly be a number
). This is such a useful behavior that to disable it would lead to lots of real-world TypeScript code breakage.
Unfortunately, it's also responsible for the behavior you're seeing here. After assigning 5
to value
, the compiler sees value
as containing a variable of the narrowed type 5
. Not Prime
. You can always widen value
to Prime
, but the compiler will not do that automatically. It thinks it is helping you by warning you that you are calling compare(5 as 5, 3)
which is forbidden.
In this case, the only way to override this behavior is with a type assertion, as you've seen. You can do this assertion either on the initial assignment, or inside the call to compare()
:
let value2: Prime = 5 as Prime
compare(value2, 3); // okay
let value3: Prime = 5;
compare(value3 as Prime, 3); // okay
Or, you could manually specify the generic type T
in your call to compare()
, which also works:
let value4: Prime = 5;
compare<Prime>(value4, 3); // okay
Any of those options are available to you.
The most canonical source of documentation I can find for this is Microsoft/TypeScript#8513, and specifically this comment.
Okay, hope that helps; good luck!