Reputation: 143
I would like to link 2 generics types in a function, and use narrowing for both types by checking one of them. What is the correct way to do this?
type A = 'A';
type B = 'B';
type AB = A | B
type ComplexType<T> = {value: T}
const f = (next: ComplexType<A>) => {}
const builder = <T extends AB>(value: T) => (next: ComplexType<T>) => {
if (value === 'A') {
f(next) // expect next is ComplexType<A> but got error
}
}
Upvotes: 14
Views: 11008
Reputation: 327849
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.
Upvotes: 12
Reputation: 134
You need to make your function f
aware of your generic too.
Changing
const f = (next: ComplexType<A>) => {}
to
const f = <T extends AB>(next: ComplexType<T>) => {}
should work.
Upvotes: -1