Generic Higher-Order Functions in TypeScript

As of TypeScript 3.1, the lib.es5.d.ts file provides a couple of predefined types that are very helpful when trying to write generic higher-order functions. In this post, I’m going to show an example of using the Parameters and ReturnType predefined types for just that purpose.

The Types

TypeScript 2.8 added the ReturnType type, and TypeScript 3.1 added the Parameters type. Here are the definitions as of TypeScript 3.1:


/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;

/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

Higher-Order Function

Here’s a higher-order function that can decorate any given function so that it logs how long the given function took to run. I don’t want to lose any type checking by decorating my functions, so the higher-order function needs to be able to preserve the types of the given function.


function logDuration<T extends (...args: any[]) => any>(func: T): (...funcArgs: Parameters<T>) => ReturnType<T> {
  const funcName = func.name;

  // Return a new function that tracks how long the original took
  return (...args: Parameters<T>): ReturnType<T> => {
    console.time(funcName);
    const results = func(...args);
    console.timeEnd(funcName);
    return results;
  };
}

Try It Out

Here’s a simple function that just adds two numbers together:


function addNumbers(a: number, b: number): number {
  return a + b;
}

addNumbers has the following type signature:


function addNumbers(a: number, b: number): number

It can be decorated using logDuration so we can monitor how long it takes to execute each time it’s called.


const addNumbersWithLogging = logDuration(addNumbers);

If you check the types on addNumbersWithLogging, you’ll see that they match the input function exactly:


const addNumbersWithLogging: (a: number, b: number) => number

And when calling the new function, you’ll now see how long it took to execute:


addNumbersWithLogging(5, 3);

> addNumbers: 0.193ms

Summary

Being able to write correctly typed, generic, higher-order functions is extremely powerful. Logging how long a function takes to execute is just one simple example, but the possible uses are endless.

Conversation
  • Randy Hudson says:

    log duration doesn’t support contextual typing that way. Here’s an approach that does:

    function logDuration R, A extends any[], R>(func: T): T {
    const funcName = func.name;

    // Return a new function that tracks how long the original took
    return function (…args: A): R {
    console.time(funcName);
    const results = func(…args);
    console.timeEnd(funcName);
    return results;
    } as T;
    }

    function strlen(): (s: string) => number {
    return logDuration(value => value.length);
    }

    • Patrick Bacon Patrick Bacon says:

      Randy – I’m not able to get your version to compile. I think something might have gotten messed up in the comment formatting. Would you mind linking to a gist (https://gist.github.com/), or codepen or something?

  • Eduardo Carvalho says:

    how would all this work if I wanted to wrap an async function?
    I’m having trouble with the return types, namely, something is returning Promise<Promise>

    • Patrick Bacon Patrick Bacon says:

      Eduardo – It should work just fine with an async function. If you pass logDuration a function that returns a Promise<string> then you’ll get back a function that returns a Promise<string>. If you’re seeing something unexpected, like a Promise<Promise<string>>, double check the type of the function you’re passing in.

  • Richard says:

    There is a problem with the ` any>` part – it will hide implicit any errors. See https://stackoverflow.com/q/63062652/1660584

  • Comments are closed.