Skip to content
Advertisement

TypeScript: Is it possible to define a variadic function that accepts different types of arguments?

I want to write a generic function that accepts variable number of arguments that may have different types and returns a tuple based on those arguments.

Here is an example in JavaScript:

function evaluate (...fns) {
  return fns.map(fn => fn())
}

evaluate(
  () => 10
) // [ 10 ]

evaluate(
  () => 10,
  () => 'f',
  () => null
) // [ 10, 'f', null ]

And in TypeScript I need to somehow convert the spread argument tuple to a resulting one:

function evaluate<T1, T2 ... Tn> (
  ...fns: [() => T1, () => T2 ... () => Tn]
): [T1, T2 ... Tn] {
  return fns.map(fn => fn()) as [T1, T2 ... Tn]
}

evaluate(
  () => 10
) // [ 10 ]: [number]

evaluate(
  () => 10,
  () => 'f',
  () => null
) // [ 10, 'f', null ]: [number, string, null]

I’ve tried a naive approach of creating an overload for all reasonable lengths of tuple:

function evaluate<T1> (
  fn1: () => T1
): [T1]
function evaluate<T1, T2> (
  fn1: () => T1,
  fn2: () => T2
): [T1, T2]
function evaluate<T1, T2, T3> (
  fn1: () => T1,
  fn2: () => T2,
  fn3: () => T3
): [T1, T2, T3]
function evaluate<T1, T2, T3> (
  ...fns: Array<(() => T1) | (() => T2) | (() => T3)>
): [T1] | [T1, T2] | [T1, T2, T3] {
  return fns.map(fn => fn()) as [T1] | [T1, T2] | [T1, T2, T3]
}

But it looks horribly, doesn’t scale well and causes issues with a more complex function body.

Is there any way this could be done dynamically? Thanks!

Advertisement

Answer

The easiest way to implement this is to make evaluate() generic in its arraylike output type T (intended to be a tuple type), and then represent the fns rest parameter as a mapped type on T, noting that mapped array/tuple types are also array/tuple types:

function evaluate<T extends any[]>(
  ...fns: { [I in keyof T]: () => T[I] }
) {
  return fns.map(fn => fn()) as T;
}

Note that the type assertion as T is necessary because the compiler cannot see that fns.map(fn => fn()) will have the effect of converting an array/tuple of function types to the array/tuple of corresponding return types. See Mapping tuple-typed value to different tuple-typed value without casts for more information.

Because {[I in keyof T]: () => T[I]} is a homomorphic mapped type where we are mapping directly over keyof T (see What does “homomorphic mapped type” mean? for more information), the compiler is able to infer T from it (linked page is deprecated, but still accurate and no new page exists 🤷‍♂️).

Let’s see it in action:

const x = evaluate(() => 10);
// const x: [number]

const y = evaluate(
  () => 10,
  () => 'f',
  () => null
)
// const y: [number, string, null]

Looks good. The compiler sees that x is of type [number] and y is of type [number, string, null]. It also behaves reasonably in cases where you pass in a rest argument of unknown order/length:

const fs = [() => "a", () => 3];
// const fs: ((() => string) | (() => number))[]

const z = evaluate(...fs);
// const z: (string | number)[]

Here fs is of the type Array<(()=>string) | (()=>number)>, and so z is of the analogous type Array<string | number>.

Playground link to code

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement