I have defined a function that takes a string name and variable number of remaining params that it should infer from the provided interface. When calling this function, the inference is done correctly, but it is not inferred within the function implementation.
interface ThingParams {
A: [isThing: boolean];
B: [thingCount: number];
}
function DoThing<T extends keyof ThingParams>( name: T, ...params: ThingParams[T] )
{
if ( name === 'A' )
{
// Why is foo typed as boolean | number instead of being correctly inferred to be boolean???
const foo = params[0]
}
}
DoThing( 'A', true ); // Correctly limits what I can pass
DoThing( 'B', 5 ); // All good
DoThing( 'B', false ); // Errors, as expected
It seems like it should be able to narrow the type of foo
based on the condition assigning a specific T
. I'm hoping to do this without balooning the types with something like
interface ThingA { name: 'A', value: [isThing: boolean] }
interface ThingB { name: 'B', ... }
type ThingParams = ThingA | ThingB;
...
when all I need is a simple mapping. Is there something I can do better in the definition of DoThing
to get it to correctly narrow the types within the implementation?
CodePudding user response:
Narrowing the type of ...params
based on the value of name
is currently not possible because the type of name
is a generic type. See #24085 for a similar issue.
Control flow analysis is often not behaving well when generic types are involved. Ideally, we should remove generics from the function. We can describe the relationship between name
and ...params
using a tuple union.
type ThingParamsUnion = {
[K in keyof ThingParams]: [name: K, ...params: ThingParams[K]]
}[keyof ThingParams]
// type ThingParamsUnion = [name: "A", isThing: boolean] | [name: "B", thingCount: number]
ThingParamsUnion
takes ThingParams
and constructs a discriminating union of tuples which describe the possible combination of parameter types. We can use ThingParamsUnion
to type the parameters of DoThing
using a spread parameter.
Now to get TypeScript to properly discrimate the union, we need some ugly destructuring
function DoThing(...args: ThingParamsUnion) {
const [name] = args
if (name === 'A')
{
const [_, ...params] = args
const foo = params[0]
// ^? foo: boolean
}
}
I initially hoped that we could destructure name
and params
like this:
function DoThing(...[name, ...params]: ThingParamsUnion) {
if (name === 'A')
{
const foo = params[0]
// ^? foo: number | boolean
}
}
But it seems like the compiler does not properly discriminate in this scenario.