Fun with Typescript and Higher Order Functions
Typing higher order functions in Typescript is fun
Functional programming is fun and building higher order functions in regular JavaScript is straightforward. Let's build one such function: once
- a function that accepts another function as parameter and returns a function that caches the result (so it is only invoked once):
function once(fn) {
let isRun = false;
let result;
return function () {
if (!isRun) {
isRun = true;
result = fn.apply(null, arguments);
}
return result;
};
}
Looks decent. If you are worried about caching result per parameters, that is a topic for another day. And a great exercise for the reader :)
It becomes slightly prettier when using ES6 and spread operator:
function once(fn) {
let isRun = false;
let result;
return function (...args) {
if (!isRun) {
isRun = true;
result = fn(...args);
}
return result;
};
}
Much better. Let's use it!
function sum(a, b) {
return a + b;
}
const sumOnce = once(sum);
Ok, so let's port it to TypeScript and add types. What type will we use for the argument? It is a function, but with variable number of parameters. We could use (...args: any[]) => any
, but that will hide the original types and make the sumOnce
useless!
Luckily, we can use type inference and higher order types ReturnType<T>
and Parameters<T>
. But what is T?
We will pretend that our function type is generic and extends functions with arbitrary number of parameters:
function once<T extends (...args: any[]) => any>(fn: T);
Now we can type result
using the higher order type:
let result: ReturnType<T>;
And instead of using ...args: any[]
we can infer the original parameters:
return function(...args: Parameters<T>) {
Which completes our typing and sumOnce
has the original (number, number) -> number
signature.
Final code is:
function once<T extends (...args: any[]) => any>(fn: T) {
let isRun = false;
let result: ReturnType<T>;
return function (...args: Parameters<T>) {
if (!isRun) {
isRun = true;
result = fn(...args);
}
return result;
};
}