Search code examples
typescriptgenericsvariadic-functions

How to type function with variadic number of generic type variables in TypeScript?


Given

type Loadable<T> = () => T
type LoadableCombinerResult<T> = { result: T }

I would like define types for a function with input like this:

  • variable number of Loadable<ResponseDataType> inputs with different ResponseDataType for each input,
  • combiner taking data results of above loadables.

The function would take care of handling error states and loading progress of loadables, plus few other things. The function would call combiner only if all loadables are successfully loaded.

This is possible to implement in untyped JavaScript. However, I fail to properly type it in TypeScript.

Non-variadic typing would look like this:

function useLoadableCombiner2<TResult, T1, T2>(
  combiner: (data1: T1, data2: T2) => TResult,
  loadable1: Loadable<T1>,
  loadable2: Loadable<T2>
): LoadableCombinerResult<TResult> { ... }

function useLoadableCombiner3<TResult, T1, T2, T3>(
  combiner: (data1: T1, data2: T2, data3: T3) => TResult,
  loadable1: Loadable<T1>,
  loadable2: Loadable<T2>,
  loadable3: Loadable<T3>
): LoadableCombinerResult<TResult> { ... }

function useLoadableCombiner4<TResult, T1, T2, T3, T4>(
  combiner: (data1: T1, data2: T2, data3: T3, data4: T4) => TResult,
  loadable1: Loadable<T1>,
  loadable2: Loadable<T2>,
  loadable3: Loadable<T3>,
  loadable4: Loadable<T4>
): LoadableCombinerResult<TResult> { ... }

function useLoadableCombinerN<...>(...): LoadableCombinerResult<TResult> { ... }

Is it possible to type this in TypeScript as one function in one declaration?

It could be an array/typed-tuple instead of variable number of arguments.

The goal is to put in variable number of loadables in, and then being able to call typed combiner with all the data, after a successful load of everything.


Solution

  • You can use a generic tuple type T to represent the rest parameter list to combiner, and then map over that tuple type to get the type of the loadable rest parameter:

    declare function useLoadableCombiner<R, T extends any[]>(
      combiner: (...data: T) => R,
      ...loadable: { [I in keyof T]: Loadable<T[I]> }
    ): LoadableCombinerResult<R>;
    

    Now when you call useLoadableCombiner, the type checker can infer T either from the parameter types of combiner or, if you don't annotate those, from the return types of each element of loadable:

    const x = useLoadableCombiner(
      //  ^? const x: LoadableCombinerResult<boolean>
      (str, num, dat) => str.length + num > dat.getTime(),
      () => "a", () => 1, () => new Date()
    )
    

    Here T is inferred from loadable is of type [Loadable<string>, Loadable<number>, Loadable<Date>], and then combiner is checked (you can see how even though we don't annotate str, num, and dat, the type checker knows their types from loadable) and returns a boolean, and thus x is of type LoadableCombinerResult<boolean>.

    Playground link to code