DEV Community

Volodymyr Yepishev
Volodymyr Yepishev

Posted on

Polish types for the new debounce function

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];
}
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>>;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

Try it live here.

The repo is here.

Packed as npm package here.

Discussion (0)