Jon Koops
Jon Koops

Reputation: 9301

Accept a mixed type tuple in a function without losing type checking

import encodeParam from './encodeParam'

interface UrlParam<T> {
  name: string
  defaultValue: T
  encode: (value: T) => string | null
  decode: (value: string) => T
}

function buildQueryString<T>(...params: [UrlParam<T>, T][]) {
  const searchParams = new URLSearchParams()

  params.forEach(([param, value]) => {
    const newValue = encodeParam(param, value)

    if (newValue) {
      searchParams.set(param.name, newValue)
    }
  })

  return searchParams.toString()
}

const one: UrlParam<string> = {
  name: 'one',
  defaultValue: '',
  encode: (val) => val,
  decode: (val) => val,
}

const two: UrlParam<number> = {
  name: 'two',
  defaultValue: 0,
  encode: (val) => val.toString(),
  decode: (val) => Number(val),
}

const query = buildQueryString(
  [one, 'foo'],
  [two, 42], // This will not compile because only UrlParam<string> is expected.
)

I've written a function called buildQueryString to encode a series of query parameters using tuples that contain the definition of the param (UrlParam) and the value that should be used. I am struggling to set up the signature of the method to accept a mixed type of UrlParam<T>, how could this be accomplished?

This should of course work so that the [UrlParam<T>, T] constraint of the tuple is still checked, so no any types are used.

Upvotes: 1

Views: 103

Answers (1)

Mingwei Samuel
Mingwei Samuel

Reputation: 3272

Playground Link

The key to typing functions like this is to make sure the generic type can contain all the information needed. With just a T, everything gets converted to unions and we lose the ordering. Instead, we can use an array/tuple generic type (which I called A) where each entry corresponds to each argument tuple:

type UrlParamPair<T> = [ UrlParam<T>, T ];
type UrlParamPairs<A extends any[]> = { [K in keyof A]: UrlParamPair<A[K]> };

function buildQueryString<A extends any[]>(...params: UrlParamPairs<A>) {
    ...
}

The UrlParamPairs mapped (array) type converts the array of types to the array of tuples, e.g. UrlParamPairs<[string, number]> becomes [ UrlParamPair<string>, UrlParamPair<number> ]

The type inference for the function call shows this working:

function buildQueryString<[string, number]>(
    params_0: UrlParamPair<string>, params_1: UrlParamPair<number>): string

Upvotes: 1

Related Questions