Search code examples
typescripttypesdebouncing

safe type debounce function in typescript


I have the following debounce function in typescript:

export function debounce<T>(
  callback: (...args: any[]) => void,
  wait: number,
  context?: T,
  immediate?: boolean
) {
  let timeout: ReturnType<typeof setTimeout> | null;

  return (...args: any[]) => {
    const later = () => {
      timeout = null;

      if (!immediate) {
        callback.apply(context, args);
      }
    };
    const callNow = immediate && !timeout;

    if (typeof timeout === "number") {
      clearTimeout(timeout);
    }

    timeout = setTimeout(later, wait);

    if (callNow) {
      callback.apply(context, args);
    }
  };
}

i'm looking for a better way to cast ...args: any[] with a safer type.

How can i change it?

UPDATE

I came out with this solution:

export function debounce<T = unknown, R = void>(
  callback: (...args: unknown[]) => R,
  wait: number,
  context?: T,
  immediate?: boolean
) {

What do you think?


Solution

  • I made some changes.

    First, we want the generic type to be a function so that later we can safely get the parameters with the Paramters utility type. Second, personally I like to have the wait time first because I frequently apply the same debounce timing to a lot of listeners with partial application, YMMV. Third, we want to return a regular (non-arrow) function with a typed this parameter so that the caller doesn't need to explicitly pass in a context.

    function debounce<T extends (...args: any[]) => void>(
      wait: number,
      callback: T,
      immediate = false,
    )  {
      // This is a number in the browser and an object in Node.js,
      // so we'll use the ReturnType utility to cover both cases.
      let timeout: ReturnType<typeof setTimeout> | null;
    
      return function <U>(this: U, ...args: Parameters<typeof callback>) {
        const context = this;
        const later = () => {
          timeout = null;
    
          if (!immediate) {
            callback.apply(context, args);
          }
        };
        const callNow = immediate && !timeout;
    
        if (typeof timeout === "number") {
          clearTimeout(timeout);
        }
    
        timeout = setTimeout(later, wait);
    
        if (callNow) {
          callback.apply(context, args);
        }
      };
    }
    

    For usage we can use for both plain functions and methods:

    const handler: (evt: Event) => void = debounce(500, (evt: Event) => console.log(evt.target));
    class Foo {
        constructor () {
            // can also type-safely decorate methods
            this.bar = debounce(500, this.bar.bind(this));
        }
    
        bar (evt: Event): void {
            console.log(evt.target);
        }
    }
    

    Even on the methods of object literals:

    interface Bar {
        a: number,
        f: () => void
    }
    
    const bar: Bar = {
        a: 1,
        f: debounce<(this: Bar) => void>(100, function() { console.log(this.a); }),
    }
    

    Playground