At the moment, directly achieving this is not feasible; TypeScript does not have the capability to alter generic types like UN
based on checking generic values such as this.useNumber
. In essence, TypeScript cannot use control flow analysis to impact generic type parameters. There is a feature request open at microsoft/TypeScript#33014 to support this functionality in the near future (as mentioned in the TypeScript 5.5 roadmap at microsoft/TypeScript#57475), but for now, it does not operate that way.
When dealing with
this.useNumber ? this.b(input) : this.a(input)
, even though
useNumber
evaluates to
true
in the true branch and
false
in the false branch,
UN
remains unaltered and cannot be restricted to either value of
true
or
false
. Consequently,
I
remains ambiguous and the code fails to work as intended.
The simplest solution for now is to resort to type assertions or similar methods to bypass this limitation.
If you aim to persuade TypeScript to adhere to the reasoning, you must demonstrate that this
represents a discriminated union where useNumber
acts as the discriminant. An approach could involve:
c(
this: { useNumber: true, b(input: X<UN>): number } |
{ useNumber: false, a(input: X<UN>): string },
input: X<UN>
) {
const result = this.useNumber ? this.b(input) : this.a(input);
return result.toString();
}
To communicate that the c
method can only be used on instances adhering to the discriminated union specified, a this
parameter is utilized. Once UN
is defined as either true
or false
, the compiler can verify this information, enabling subsequent calls like:
const n = new A(true);
n.c(3); // acceptable
const s = new A(false);
s.c("abc"); // acceptable
This method functions, but deviates significantly from typical TypeScript conventions. It seems to fight against the language's design. Notably, class declarations are incompatible with discriminated unions. If working with classes, utilizing inheritance for polymorphism and creating a union of subclasses may be more conventional:
abstract class ABase {
a(input: string): string {
return `${input}j`;
}
b(input: number): number {
return input * 2;
}
}
class ATrue extends ABase {
readonly useNumber = true;
c(input: number) {
return this.b(input).toString();
}
}
class AFalse extends ABase {
readonly useNumber = false;
c(input: string) {
return this.a(input).toString();
}
}
type A = ATrue | AFalse;
let a: A;
a = new ATrue();
a.c(3); // acceptable
a = new AFalse();
a.c("abc"); // acceptable
The preferred path depends on your specific scenario. If consolidating this behavior within a single class is imperative, it may necessitate unconventional approaches or employing type assertions.
Playground link to modified code