Home > Back-end >  Typescript type inference with union of tuple types
Typescript type inference with union of tuple types

Time:11-28

Given the following types

type Tool = 'network_query' | 'cluster'
type Network = 'one' | 'two' | 'three'

class QueryOneParams {...}
class QueryTwoParams {...}
class QueryThreeParams {...}
class ClusterParams {...}

I'm trying to define a mapping between combinations of Tool and Network so that I could write something like this:

const queryParamsLookup = {
    'one': QueryOneParams,
    'two': QueryTwoParams,
    'three': QueryThreeParams
}

type Job<Tool, Network> = {
    id: string,
    params: JobParams<Tool, Network>
}

where

  • JobParams<'network_query', 'one'> resolves to QueryOneParams
  • JobParams<'network_query', 'two'> resolves to QueryTwoParams
  • JobParams<'network_query', 'three'> resolves to QueryThreeParams
  • JobParams<'cluster'> resolves to ClusterParams
  • JobParams<'cluster', 'one'>, JobParams<'cluster', 'two'> and JobParams<'cluster', 'three'> are invalid

That would require me to somehow define that the second generic parameter 'one' | 'two' | 'three' is only used and required, if the first parameter is 'network_query'. Afaik, Typescript does not support optionally defined generic parameters based on the type of another parameter.

Is that correct? I'd love to be wrong here :)

As an alternative, I have defined a helper type like so:

type NetworkQueryJobType = {
    [N in keyof typeof queryParamsLookup]: ['network_query', N]
}[keyof typeof queryParamsLookup]
// ['network_query', 'one'] | ['network_query', 'two'] | ['network_query', 'three']

type JobType = NetworkQueryJobType | ['cluster']
// ['network_query', 'one'] | ['network_query', 'two'] | ['network_query', 'three'] | ['cluster']

and changed the definition of Job to

type Job<JobType> = {
    id: string,
    params: JobParams<JobType>
}

with this approach, I'm having trouble getting type inference to work properly in the mapper type JobParams:

type JobParams<T extends JobType> = T extends ['network_query', infer N] ?
typeof queryParamsLookup[N] // Error: N is not compatible with 'one' | 'two' | 'three'
: ClusterParams

I can work around the type inference issue with:

type JobParams<T extends JobType> = T extends ['network_query', infer N] ?
N extends Network ?
    typeof queryParamsLookup[N] // No error
    : ClusterParams
: ClusterParams

All of that however still leaves me with poor autocomplete performance when typing, for example:

const params: JobParams<['

VSCode will not suggest 'cluster' | 'network_query'

All in all, I feel like fighting a losing battle here. Am I doing something fundamentally wrong?

Playground Link

CodePudding user response:

You can use generic parameter defaults and a neutral type, such as null to match the provided behavior.

I have made some assumptions about your code, please correct me if there is something I interpreted incorrectly.

Firstly, queryParamsLookup is a type QueryParamsLookup:

type QueryParamsLookup = {
    'one': QueryOneParams,
    'two': QueryTwoParams,
    'three': QueryThreeParams
}

defining the object as it is defined in the question will resolve the types to the types of provided class constructors, which is probably not what was intended as that would be quite rare.

The JobParams type can then be defined as:

type JobParams<T extends Tool, N extends T extends "network_query" ? Network : null = T extends "network_query" ? Network : null> =
    N extends Network ? QueryParamsLookup[N] : ClusterParams

Which would fulfill the following requirements:

// valid cases
const one: JobParams<"network_query", "one"> = new QueryOneParams()
const two: JobParams<"network_query", "two"> = new QueryTwoParams()
const three: JobParams<"network_query", "three"> = new QueryThreeParams()
const cluster: JobParams<"cluster"> = new ClusterParams()

// invalid cases
const inv1: JobParams<"cluster", "one"> = new QueryOneParams()
const inv2: JobParams<"cluster", "two"> = new QueryTwoParams()
const inv3: JobParams<"cluster", "three"> = new QueryThreeParams()

This definition of JobParams is pessimistic when a union is provided as its first argument, meaning it will treat the union as if it was not network_query. This behaviour can be inverted by flipping the ternary operators.

A link to a playground with the solution.

While this is perfectly possible to do with a tuple generic as well, it is at quite excessive in my opinion and not very intuitive.

  • Related