How to assign Array#map results to variadic tuple `[P in keyof T]: ReturnType<T[P]>`?

368 views Asked by At

I'm trying to create a strongly-typed function that maps over a homogeneous array of functions that return an arbitrary value.

I've experimented with both of these, and while they both successfully return the correct types, I get errors in the return value:

const runAll = <T extends (() => any)[]>(
    array: [...T],
): {
    [P in keyof T]: ReturnType<T[P]>
} => {
    const result = [];
    for (let i = 0; i < array.length; i += 1) {
        result.push(array[i]());
    }
    return result;
}
// ^ Error: Type 'any[]' is not assignable to type '{ [P in keyof T]: ReturnType<T[P]>; }'.ts(2322)

const results = runAll([
    () => 'hello',
    () => 123,
    () => true,
]);

// const results: [string, number, boolean]

const runAll = <T extends (() => unknown)[]>(
    actions: [...T],
): {
    [P in keyof T]: ReturnType<T[P]>;
} => actions.map(action => action());

// ^ Error: Type 'unknown[]' is not assignable to type '{ [P in keyof T]: ReturnType<T[P]>; }'.ts(2322)


const results = runAll([
    () => 'hello',
    () => 123,
    () => true,
]);
// const results: [string, number, boolean]

What is the correct way to match the return type?

2

There are 2 answers

0
kdau On BEST ANSWER

The simplest (and perhaps only) way to do this is to explicitly cast from the unknown[] returned by .map to the complex return type of runAll. I know that feels wrong, but you know that the cast will always be safe, so it's not terrible to make it internally within the function. I went off your second example, and also fixed a secondary error I got from the return type. (I probably have a newer TS version.) The result:

type AllResults<T> = {
    [P in keyof T]: T[P] extends (() => unknown) ? ReturnType<T[P]> : never;
};

const runAll = <T extends Array<() => unknown>>(
    actions: [...T],
): AllResults<T> => actions.map(action => action()) as AllResults<T>;

const results = runAll([
    () => 'hello',
    () => 123,
    () => true,
]);
// const results: [string, number, boolean]
0
captain-yossarian from Ukraine On

COnsider next example:


type Fn = () => any

type MapPredicate<F> = F extends Fn ? ReturnType<F> : never

/**
 * Iterate through [array] argument and get return type
 */
type Mapped<
  Arr extends Array<unknown>,
  Result extends Array<unknown> = []
  > = Arr extends []
  ? []
  : Arr extends [infer H]
  ? [...Result, MapPredicate<H>]
  : Arr extends [infer Head, ...infer Tail]
  ? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
  : Readonly<Result>;

/**
 * Overload
 * see docs https://www.typescriptlang.org/docs/handbook/functions.html#overloads
 */
function runAll<Cb extends Fn, Cbs extends Cb[]>(
  array: [...Cbs],
): Mapped<Cbs>

function runAll<Cb extends Fn, Cbs extends Cb[]>(
  array: [...Cbs],
) {
  return array.reduce<ReadonlyArray<ReturnType<Cbs[number]>>>(
    (acc, elem) => [...acc, elem()],
    []
  )
}

const results = runAll([
  () => 'hello',
  () => 123,
  () => true,
]); // [string, number, boolean]

Playground

Explicit return type usually does not work for functions where you iterate through an array. It is better to use overloads here

I added Mapped helper. IT just recursively iterates through type of array argument and returns return type of callback.

Similar to array.map(elem=>elem())

Here, in my blog, you can find more interesting examples