Article summary
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.
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);
}
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?
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>
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.
There is a problem with the ` any>` part – it will hide implicit any errors. See https://stackoverflow.com/q/63062652/1660584