'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!
Solution 1:[1]
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>.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | jcalz |
