Here:
function foo<T extends boolean> ( multiple: T, value: T extends true ? number[] : number) {
if (multiple) {
let test: number[] = value; // Type 'number' is not assignable to type 'number[]'.(2322)
}
}
I was expecting inside if
, value to be resolved as number[]
no?
Initially had tried without generics too but had same issues:
function foo ( multiple: boolean, value: typeof multiple extends true ? number[] : number) {
if (multiple) {
let test: number[] = value; // Type 'number' is not assignable to type 'number[]'.(2322)
}
}
TypeScript can't currently mix generics with control flow analysis very well. When you check multiple
, TypeScript can narrow multiple
from T
to, say, T & true
. But it cannot narrow or re-constrain T
itself. You'd like to say that if multiple
is true
then T
must also be true
. But that's actually not the case, as you can verify by calling:
foo(Math.random() < 0.99, 1); // no compiler error, 99% chance of failure
Here Math.random() < 0.99
is of type boolean
which is the union type true | false
, so the distributive conditional type T extends true ? number[] : number
evaluates to the union number[] | number
, and suddenly foo()
accepts possibly mismatched arguments.
There is a longstanding open feature request at microsoft/TypeScript#27808 to allow someone to have a generic function where T
is either true
or false
and cannot be the union true | false
. But for now it's not part of the language. Additionally there's the general problem whereby TypeScript really can't see if some value is or is not assignable to a generic conditional type. The feature request at microsoft/TypeScript#33912 deals with that. Until and unless these are implemented, you'll have to change what you're doing.
In general the only surefire way to work around this is with type assertions. But in this case your function's return type does not depend on its input (it's always void
) and the argument list looks like a discriminated union [true, number[]] | [false, number]
because true
and false
are valid discriminants.
So you can refactor your code to use control flow analysis instead of generics. The function can accept a rest parameter whose type is a destructured discriminated union of tuple types:
function foo(...[multiple, value]:
[multiple: true, value: number[]] |
[multiple: false, value: number]
) {
if (multiple) {
let test: number[] = value; // okay
}
}
That works because TypeScript now understands that checking multiple
will narrow the discriminated union represented by [multiple, value]
. When you call foo()
it looks like an overloaded function:
foo(^
// 1/2 foo(multiple: true, value: number[]): void
// 2/2 foo(multiple: false, value: number): void
And you can call it in the ways you want
foo(true, [1, 2, 3]);
foo(false, 4);
And if you call it in invalid ways you get an error, even in the Math.random()
case:
foo(true, 5); // error
foo(Math.random() < 0.99, 1); //error
That's because [boolean, number]
is not assignable to [true, number[]] | [false, number]
.