How to infer function return type based on an function array argument?

Viewed 182

Given a plain JS function that takes in an array of callbacks (that each returns an object), and then returns their combined output:

export const applyCallback = (callbacks) => {
  return callbacks.reduce((acc, callback) => { ...acc, ...callback() }, {});
}

How would one appropriately type this using TypeScript to infer the return type of applyCallback? Although I am aware of ReturnType, it applies to a single function, meanwhile here I would want to get a ReturnType of each callback and create a union of their return types.

2 Answers

It can be done with some overloads:


type Fn = () => object

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;


function applyCallback<R extends Fn, U extends ReadonlyArray<R>>(...callbacks: U): UnionToIntersection<ReturnType<U[number]>>;
function applyCallback(...callbacks: Fn[]) {
  return callbacks.reduce((acc, callback) => {
    return { ...acc, ...callback() }
  }, {});
}


const result = applyCallback(
  () => ({ age: 42 }),
  () => ({ name: 'John' })
); // {age:42} & {name:'John'}

Playground

It is hard to narrow the initial value of reduce for TypeScript. Most of the type people are using type assertion as, but in this case type assertion does not help so I decided to use overloading.

You can find more information about typing callbacks in my blog

To address the "in this case type assertion does not help" from the captain-yossarian's answer (completely fine otherwise): it is not necessary to return an intersection of objects and entirely possible to build up the desired object with the help of exactly the as assertion.

What's more, if one is content with the returned type only having optional properties, the assertion can be avoided entirely (but due to how the reduce method signature is typed, it is nigh impossible to ensure the compiler that it is safe to jump from an empty object to a fully populated one).

With the help of some key remapping and mapped types, the task becomes fairly trivial0.

type AnyFunc = (...args:any[]) => any;

function applyCallback<T extends AnyFunc[]>(callbacks: T) {
  return callbacks.reduce((acc, callback) => {
    return { ...acc, ...callback() }
  }, {} as { [ P in ReturnType<T[number]> as keyof P ] : P[keyof P] });
}


const result = applyCallback([
  () => ({ age: 42 }),
  () => ({ name: 'John' })
]); /* { age: number; name: string; } */

Playground


0 Note that the applyCallback signature follows the one in the question that accepts a single function array instead of rest parameters.

Related