It's probably a missing feature.
When you check (x === "A")
it acts as a type guard on the type of the value x
, causing it to narrow from the union type "A"|"B"
to just "A"
via control flow analysis.
Unfortunately, generic type parameters in TypeScript don't get narrowed via control flow analysis; see microsoft/TypeScript#24085 for more information. So, for example, checking (key === "A")
will not narrow the type of K
to "A"
. This sort of restriction makes sense when you have multiple values of the same generic type:
function foo<K extends Union>(key: K, x: Union, key2: K) {
if (key2 === "A") {
Global[key].toFixed(); // error!
}
}
Obviously checking the value of key2
should have no effect on the type of key
, so the compiler conservatively does not ever think K
should be narrowed. This is the basic problem in microsoft/TypeScript#13995 and multiple related issues have been raised with suggestions about how to handle it for the cases where it should be safe to do such narrowing. So far nothing has made it into the language, though.
This isn't really the full story, though; one could counter with: okay, maybe you can't narrow the type parameter K
from K extends Union
to K extends "A"
, but surely you can narrow the type of the value key
from K
to "A"
or K & "A"
(an intersection type), which would make Global[key].toFixed()
succeed:
if (key === "A") {
Global[key as (K & "A")].toFixed(); // okay
}
And I don't have a good answer to that right now. Most questions I've seen about this end up being referred eventually to microsoft/TypeScript#13995. ♂️
The closest I can come to a full answer is that it seems that using built-in type guards like a === b
or typeof a === "string"
or a instanceof B
or a in b
only ends up filtering unions or possibly narrowing string
or number
to string or number literals, but never produces an intersection type. I've asked before, see microsoft/TypeScript#21732, for the a in b
type guard to produce some intersections, but it hasn't been implemented. So this might be two missing features:
- no narrowing generic type parameters, and
- no built-in type guard narrowing to intersections.
So, workarounds: Obviously the easiest one for this example is just to reassign the generic-typed variable's value to a union-typed variable:
const k: Union = key;
if (k === "A") {
Global[k].toFixed();
}
Or, you could use a type assertion as in the above as (K & "A")
or just as "A"
:
if (key === "A") {
Global[key as (K & "A")].toFixed(); // okay
Global[key as "A"].toFixed(); // okay
}
Or, if this happens a lot, you could write your own user-defined type guard function, since user-defined type guards do produce intersections in the true
branch of the subsequent control flow:
const isEq =
<T extends string | number | boolean>(v: any, c: T): v is T => v === c;
if (isEq(key, "A")) {
Global[key].toFixed(); // okay, key is now of type K & "A";
}
Playground link to code