There is currently no way to narrow a type parameter like T
by checking a value like value
. It is possible that value === "A"
is true but that does not mean T
is "A"
. After all, maybe value
is of type "A" | "B"
, say by pass ing in an expression where the compiler infers the full union type:
builder(Math.random() <= 0.999 ? "A" : "B") // no error
Here there is a 99.9% chance that you've passed in "A"
but it's still possible that you've passed in "B"
. The compiler infers that T
is "A" | "B"
. And therefore the next
parameter will be of type ComplexType<"A" | "B">
. So there is no compiler error when you call this:
builder(Math.random() <= 0.999 ? "A" : "B")({ value: "B" }); // no error
which means the compiler is technically correct that f(next)
might be in error.
There are multiple existing issues in GitHub asking for support for narrowing type parameters inside generic function bodies. The most relevant for your code is probably microsoft/TypeScript#27808. This asks for some way to tell the compiler that T
should be either "A"
or "B"
and not "A" | "B"
. Maybe the syntax would be something like T extends_oneof [A, B]
or (T extends A) | (T extends B)
or something completely different. Then perhaps when you test value === "A"
the compiler would conclude that T extends A
, and everything would work. Alas, there is currently no such support.
For now then you just have to work around it. If you're fairly confident nobody is going to call your builder()
incorrectly, you could just use a type assertion and move on:
const builder = <T extends AB>(value: T) => (next: ComplexType<T>) => {
if (value === 'A') {
f(next as ComplexType<A>) // okay
}
}
If you really need to prevent callers from doing the wrong thing with builder()
you could make increasingly complicated call signatures that amount to simulating the "extends one of" constraint, like:
type NoneOf<T, U extends any[]> =
[T] extends [U[number]] ? never : T;
type OneOf<T, U extends any[]> =
U extends [infer F, ...infer R] ? [T] extends [F] ? NoneOf<T, R> : OneOf<T, R> : never;
const builder = <T extends "A" | "B">(
value: T & OneOf<T, ["A", "B"]>
) => (next: ComplexType<T>) => {
if (value === 'A') {
f(next as ComplexType<"A">)
}
}
builder(Math.random() <= 0.999 ? "A" : "B"); // error now
builder2("A") // okay
builder2("B") // okay
but of course the compiler can't follow that inside the body of builder
anyway (generic conditional types are hard for it to deal with) so you still need the type assertion. Personally I'd just use your original signature with the type assertion and only revisit anything more complex if you run into invalid calls in practice.
Playground link to code
T
isAB
itself: playground – Anaphrodisiacbuilder(Math.random()<=0.999 ? "A" : "B")({value: "B"})
and having a high chance of the wrong thing happening. This issues is microsoft/TypeScript#27808 and the workaround for now is probably just "use a type assertion". – Pursuer