DEV Community

Volodymyr Yepishev
Volodymyr Yepishev

Posted on

5 3

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.

Top comments (0)

typescript

11 Tips That Make You a Better Typescript Programmer

1 Think in {Set}

Type is an everyday concept to programmers, but it’s surprisingly difficult to define it succinctly. I find it helpful to use Set as a conceptual model instead.

#2 Understand declared type and narrowed type

One extremely powerful typescript feature is automatic type narrowing based on control flow. This means a variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type.

#3 Use discriminated union instead of optional fields

...

Read the whole post now!