There is always a room for improvement, so let's take a look what can be improved in the debounce function we've created in the previos article:
// debounce.function.ts
export function debounce<A = unknown, R = void>(
fn: (args: A) => R,
ms: number
): [(args: A) => Promise<R>, () => void] {
let timer: NodeJS.Timeout;
const debouncedFunc = (args: A): Promise<R> =>
new Promise((resolve) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
resolve(fn(args));
}, ms);
});
const teardown = () => clearTimeout(timer);
return [debouncedFunc, teardown];
}
It does the job, but looks clunky:
- timer type tied to Nodejs;
- does not allow multiple primitive arguments, i.e. two numbers;
- return type is hard to read.
We'll start with the easiest one, the timer type. Instead of typing it using NodeJS.Timeout
we could type it in a more sly way with ReturnType
:
let timer: ReturnType<typeof setTimeout>;
So timer is whatever setTimeout
returns, no arguing that.
Now perhaps to the most interesting part: allow passing to the debounce function any amount of arguments of any type instead of one stictly typed object.
To get there first we need to understand an interface that is applicaple to any function in typescript, if we gave it a name, and let's say, called it FunctionWithArguments
, it would look the following way:
// ./models/function-with-arguments.model.ts
export interface FunctionWithArguments {
(...args: any): any;
}
This single interface will allow us to eliminate the necessity to type separately the argument type and the return type in debounce<A = unknown, R = void>
, we could go straight to a type that expects a function instead of argument + return type, which would look like this: debounce<F extends FunctionWithArguments>(fn: F, ms: number)
.
So we get a function F
which is an extension of FunctionWithArguments
, how would a debounce function interface look like then? It would take the aforementioned function and utilise types Parameters
and ReturnType
generics to unpack whatever arguments and return type the F
function carries:
// ./models/debounced-function.model.ts
import { FunctionWithArguments } from './function-with-arguments.model';
export interface DebouncedFunction<F extends FunctionWithArguments> {
(...args: Parameters<F>): Promise<ReturnType<F>>;
}
As you can see, DebouncedFunction
accepts any function, and produces a function which is its async version, without the need to explicitly pass arguments and return types.
Having dealt with the first two points, it is time now to make return type of debounce
a bit more readable.
[(args: A) => Promise<R>, () => void]
basically equals to Array<DebouncedFunction<F> | (() => void)>
, so we can strictly type it by creating a separate interface:
// ./models/debounce-return.model.ts
import { DebouncedFunction } from './debounced-function.model';
import { FunctionWithArguments } from './function-with-arguments.model';
export interface DebounceReturn<F extends FunctionWithArguments> extends Array<DebouncedFunction<F> | (() => void)> {
0: (...args: Parameters<F>) => Promise<ReturnType<F>>;
1: () => void;
}
There we go, a strictly typed tuple.
Putting it all together we get a better typed debounce function, which no longer requires passing argument and return type explicitly, but infers them from the passed function insead:
// debounce.function.ts
import { DebouncedFunction, DebounceReturn, FunctionWithArguments } from './models';
export function debounce<F extends FunctionWithArguments>(fn: F, ms: number): DebounceReturn<F> {
let timer: ReturnType<typeof setTimeout>;
const debouncedFunc: DebouncedFunction<F> = (...args) =>
new Promise((resolve) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
resolve(fn(...args as unknown[]));
}, ms);
});
const teardown = () => {
clearTimeout(timer);
};
return [debouncedFunc, teardown];
}
Try it live here.
The repo is here.
Packed as npm package here.
Top comments (0)