myol
myol

Reputation: 9828

Return typings for dynamic functions

I have a function whose only logic is to execute a function that is passed in.

const funcRunner = async (func: Function) => {
   return await func()
}

I know that whatever the response of the function that is passed in will be the response of this same function.

However, this function return signature is inferred as Promise<any>. Is there any way to keep the response type of the original function?

const bool = async isExample(): Promise<boolean> => true;

// result should be boolean, not any
const result = await funcRunner(isExample);

Upvotes: 1

Views: 85

Answers (2)

T.J. Crowder
T.J. Crowder

Reputation: 1074148

You can make it generic, so TypeScript can infer from the call site, and use the utility type ReturnType to get its return type:

const funcRunner = async <FuncType extends () => any>(
    func: FuncType
): Promise<ReturnType<FuncType>> => {
   return await func();
};

Usage example:

async function example1() {
    return 1;
}

async function example2() {
    return "a";
}

(async() => {
    const result1 = await funcRunner(example1);
    //    ^? const result1: number
    const result2 = await funcRunner(example2);
    //    ^? const result2: string
})();

Playground link

If you like, you can make funcRunner slightly more general-purpose by not making it async, and then it could run either async or non-async functions without adding a promise to the non-async ones:

const funcRunner = <FuncType extends () => any>(
    func: FuncType
): ReturnType<FuncType> => {
    return func();
};

Playground link

You may not want to do that if you want to be sure to see funcRunner in the async stack trace, but if you're not bothered about that, you're good.


Just for fun: If you wanted to let funcRunner call functions that expect parameters, you can add an args argument that's fully typed:

async function funcRunner<FuncType extends (...args: any[]) => any>(
    func: FuncType,
    args: Parameters<FuncType>
): Promise<ReturnType<FuncType>> {
    return await func(...args);
}

If you want to make that second argument optional for functions that don't take any parameters, I think you need to overload it:

async function funcRunner<FuncType extends () => any>(
    func: FuncType,
): Promise<ReturnType<FuncType>>;
async function funcRunner<FuncType extends (...args: any[]) => any>(
    func: FuncType,
    args: Parameters<FuncType>
): Promise<ReturnType<FuncType>>;
async function funcRunner<FuncType extends (...args: any[]) => any>(
    func: FuncType,
    args?: Parameters<FuncType>
): Promise<ReturnType<FuncType>> {
    if (args) {
        return await func(...args);
    }
    return await func();
}

Usage examples for the overloaded one:

async function example1() {
    return 1;
}

async function example2() {
    return "a";
}

function example3() {
    return Math.random() < 0.5;
}

function example4(num: number) {
    return num * 42;
}

(async() => {
    const result1 = await funcRunner(example1);
    //    ^?
    const result2 = await funcRunner(example2, []); // <=== Args is allowed even when optional...
    //    ^?
    const badResult1 = await funcRunner(example2, ["x"]); // <=== ...but it can't have anything in it
    //    ^?
    const result3 = await funcRunner(example3);
    //    ^?
    const result4 = await funcRunner(example4, [3]);
    //    ^?
    const badResult2 = await funcRunner(example4); // <=== Error as desired, need args!
    //    ^?
    const badResult3 = await funcRunner(example4, ["x"]); // <=== Error as desired, wrong type!
    //    ^?
})();

Playground link

And of course, you can always make that one async-neutral as well; playground link.

Upvotes: 1

Kevin Messiaen
Kevin Messiaen

Reputation: 26

You can define a generic type to run function:

type Runnable<T> = () => Promise<T>;

And then apply it for your funcRunner

async function funcRunner<T>(runnable: Runnable<T>): Promise<T> {
  return await runnable();
}

Optionally you can use inline typing:

async function funcRunner<T>(runnable: () => Promise<T>): Promise<T> {
  return await runnable();
}

Example usage:

async function example1(): Promise<number> {
  return 1;
}


funcRunner(example1)
    .then((result: number) => console.log(result))

Upvotes: 1

Related Questions