Home > Back-end >  Why isn't TypeScript inferring rest params correctly in function implementation?
Why isn't TypeScript inferring rest params correctly in function implementation?

Time:12-06

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.


Playground

  • Related