Issues with Generics and Case Analysis Compatibility
The current state of TypeScript does not allow for control flow analysis to influence a generic type parameter, such as T
within the scope of myFn
. One major challenge is that when a type parameter is restricted to a union type like
T extends "a" | "b"
, it doesn't guarantee that
T
will strictly be either
"a"
or
"b"
; instead,
T
could potentially represent the entire union
"a" | "b"
. Currently, there's no way to enforce that
T
must specifically be only one member of a union. Thus, calling
myFn()
leads to ambiguity:
myFn(Math.random() < 0.999 ? "a" : "b", "2"); // valid
// function myFn<"a" | "b">(valueType: "a" | "b", value: "1" | "2"): void
In this scenario, T
is inferred as "a" | "b"
, and due to the distributive nature of conditional types like
T extends "a" ? "1" : "2"
, both
valueType
and
value
are independent union types. Consequently, nothing prohibits using
myFn()
with mismatched values.
An enhancement request is pending at microsoft/TypeScript#27808 aiming to impose strict constraints on by specifying that T
should precisely belong to a single union member. However, until TypeScript evolves, manipulating a value like valueType
within a generic type T
won't impact T
itself.
Synergy Between Case Analysis and Discriminated Unions
Generics cannot facilitate control flow adjustments.
If leveraging control flow analysis to evaluate one variable and affect another, utilize the first variable as a discriminant in a discriminated union type, destructure both variables:
Minus destructuring, consider the conventional discriminated union illustration:
type AcceptableArgs =
{ valueType: "a", value: "1" } |
{ valueType: "b", value: "2" };
function myFn(arg: AcceptableArgs) {
if (arg.valueType === 'a') {
arg.value // "1"
}
}
This setup explicitly conveys the correlation between valueType
and value
. Either valueType
is "a"
paired with "1"
, or it's "b"
associated with "2"
.
To circumvent passing an object to myFn
, convert AcceptableArgs
from a plain object type to a tuple type corresponding to myFn
's rest parameter list:
type AcceptableArgs =
[valueType: "a", value: "1"] |
[valueType: "b", value: "2"];
function myFn(...[valueType, value]: AcceptableArgs) {
if (valueType === 'a') {
value // value: "1"
}
}
By deconstructing the rest parameter into valueType
and value
variables, the functionality aligns with expectations. Alternatively, configure it as:
type MyFn = (...args: AcceptableArgs) => void;
const myFn: MyFn = (valueType, value) => {
if (valueType === 'a') {
value // value: "1"
}
}
Either approach allows the compiler to narrow down value
to either "1"
or "2"
based on valueType
's value. Moreover, your IDE presents the function call similar to overloading for convenient usage from the caller’s perspective:
myFn(
// ^-- 1/2 myFn(valueType: "a", value: "1"): void
// 2/2 myFn(valueType: "b", value: "2"): void
Hence, callers benefit from clarity while interacting with the function.
Explore the code further in TypeScript Playground