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