I'm trying a very basic (contrived) conditional type function and getting unexpected errors:
function test<
T
>(
maybeNumber: T
): T extends number ? number : string {
if (typeof maybeNumber === 'number') {
return maybeNumber // Type 'T & number' is not assignable to type 'T extends number ? number : string'.
}
return 'Not a number' // Type '"Not a number"' is not assignable to type 'T extends number ? number : string'.
}
I thought that this was a pretty straightforward usage of a conditional type so not sure what's going on. Any ideas?
To clarify, I’m not really trying to implement this specific function. I’m just experimenting with conditional types and want to better understand why this doesn’t actually work.
The underlying issue is that TypeScript's compiler does not narrow the type of a generic type variable via control flow analysis. When you check (typeof maybeNumber === "number")
, the compiler can narrow the value maybeNumber
to number
, but it does not narrow the type parameter T
to number
. And therefore it cannot verify that it's safe to assign a number
value to the return type T extends number ? number : string
. The compiler would have to perform some analysis it currently does not do, such as "okay, if typeof maybeNumber === "number"
, and we inferred T
from the type of maybeNumber
alone, then inside this block we can narrow T
to number
, and therefore we should return a value of type number extends number ? number : string
, a.k.a., number
". But this doesn't happen.
This is quite a pain point with generic functions with conditional return types. The canonical open GitHub issue about this is probably microsoft/TypeScript#33912, but there are a bunch of other GitHub issues out there where this is the main problem.
So that's the answer to "why doesn't this work"?
If you're not interested in refactoring to get this to work, you can ignore the rest, but it might still be instructive to know what to do in this situation instead of waiting for the language to change.
The most straightforward workaround here that maintains your call signature is to make your function a single signature overload where the implementation signature is not generic. This essentially loosens the type safety guarantees inside the implementation:
type MyConditional<T> = T extends number ? number : string;
type Unknown = string | number | boolean | {} | null | undefined;
function test<T>(maybeNumber: T): MyConditional<T>;
function test(maybeNumber: Unknown): MyConditional<Unknown> {
if (typeof maybeNumber === 'number') {
const ret: MyConditional<typeof maybeNumber> = maybeNumber;
return ret;
}
const ret: MyConditional<typeof maybeNumber> = "Not a number";
return ret;
}
Here I've gone about as far as I can go to try to guarantee type safety, by using a temporary ret
variable annotated as MyConditional<typeof maybeNumber>
which uses the control-flow-analysis-narrowed type of maybeNumber
. This will at least complain if you switch around the check (turn ===
into !==
to verify). But usually I just do something simpler like this and let the chips fall where they may:
function test2<T>(maybeNumber: T): MyConditional<T>;
function test2(maybeNumber: any): string | number {
if (typeof maybeNumber === 'number') {
return maybeNumber;
}
return "Not a number";
}
Okay, hope that helps; good luck!
Playground link to code
Correct answer and current workaround (at the time of writing):
type MaybeNumberType<T extends number | string> = T extends number
? number
: string;
function test<T extends number | string>(
maybeNumber: T,
): MaybeNumberType<T> {
if (typeof maybeNumber === 'number') {
return <MaybeNumberType<T>>(<unknown>maybeNumber);
}
return <MaybeNumberType<T>>(<unknown>'Not a number');
}
test(3); // 3
test('s'); // Not a number
Your function would be better off if implemented with overloads:
function test(arg: number): number;
function test(arg: unknown): string;
function test(arg: any): string | number {
if (typeof arg === 'number') {
return arg;
}
return 'Not a number'
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With