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

Time:05-03

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!

CodePudding user response:

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

  • Related