Implementing a Flexible Debounce Function in TypeScript

Dhruv KharaDhruv Khara
4 min read

Introduction

In the world of web development, performance optimization is crucial. One common technique for improving performance and reducing unnecessary computations is debouncing. In this article, we'll dive deep into a flexible implementation of a debounce function using TypeScript.

What is Debouncing?

Before we jump into the code, let's briefly explain what debouncing is and why it's useful.

  • Definition: Debouncing is a programming practice used to ensure that time-consuming tasks do not fire so often, making it particularly useful for performance optimization.

  • Use case: It's commonly used with event listeners, such as resizing windows or typing in input fields.

  • How it works: When a debounced function is called, it waits a specified amount of time before executing. If the function is called again within this waiting period, the timer resets.

The Code: A Flexible Debounce Implementation

Let's break down our TypeScript implementation of a debounce function:

export function debounce<T extends (...args: Parameters<T>) => ReturnType<T>>(
  callback: T,
  delay: number,
) {
  let timer: ReturnType<typeof setTimeout>;

  return (...args: Parameters<T>) => {
    const p = new Promise<ReturnType<T> | Error>((resolve, reject) => {
      clearTimeout(timer);

      timer = setTimeout(() => {
        try {
          const output = callback(...args);
          resolve(output);
        } catch (err) {
          if (err instanceof Error) {
            reject(err);
          } else {
            reject(new Error(`An error has occurred: ${String(err)}`));
          }
        }
      }, delay);
    });

    return p;
  };
}

Debounce: Our Modern TypeScript Approach vs. Popular Libraries

This TypeScript debounce implementation provides a modern, Promise-based approach that is different from traditional libraries like Lodash or Underscore.js. Key features include:

  1. Concise core functionality

  2. Built-in error handling

  3. Seamless async/await integration

  4. Native TypeScript support for better type safety

While Lodash provides additional options like leading/trailing calls and maximum wait times, it comes with increased complexity. Our implementation balances simplicity and functionality, making it ideal for modern projects prioritizing readability and straightforward async operations.

Popular libraries may offer more optimizations for edge cases, but our solution is well-suited for most JavaScript and TypeScript projects without sacrificing essential features.

Breaking Down the Implementation

1. Function Signature

export function debounce<T extends (...args: Parameters<T>) => ReturnType<T>>(
  callback: T,
  delay: number,
)
  • Our debounce function is generic, allowing it to work with any function type.

  • It takes two parameters: the callback function to debounce and the delay in milliseconds.

2. Timer Variable

let timer: ReturnType<typeof setTimeout>;
  • We declare a variable to hold the timer ID, which we'll use to clear and set timeouts.

3. Returned Function

return (...args: Parameters<T>) => { ... }
  • The debounce function returns a new function that wraps our original callback.

  • This returned function uses the spread operator to accept any number of arguments.

4. Promise Creation

const p = new Promise<ReturnType<T> | Error>((resolve, reject) => { ... });
  • We create a Promise to handle the asynchronous nature of our debounced function.

  • This allows us to use the debounced function with async/await or .then() syntax.

5. Clearing Previous Timer

clearTimeout(timer);
  • Before setting a new timer, we clear any existing one. This is the core of the debounce functionality.

6. Setting New Timer

timer = setTimeout(() => { ... }, delay);
  • We set a new timer that will execute after the specified delay.

7. Executing the Callback

try {
  const output = callback(...args);
  resolve(output);
} catch (err) {
  if (err instanceof Error) {
    reject(err);
  } else {
    reject(new Error(`An error has occurred: ${String(err)}`));
  }
}
  • Inside the timer, we try to execute the callback function.

  • If successful, we resolve the promise with the output.

  • If an error occurs, we reject the promise with the error.

8. Returning the Promise

return p;
  • Finally, we return the promise, allowing the caller to handle the result asynchronously.

Practical Applications

Here are some common scenarios where you might use this debounce function:

  1. Search Input: Delay API calls while the user is typing.

  2. Window Resize: Prevent excessive calculations during window resizing.

  3. Scroll Events: Optimize performance for scroll-based animations or loading.

Conclusion

Debouncing is a powerful technique for optimizing performance in web applications. This TypeScript implementation provides a flexible, type-safe, and promise-based approach to debouncing that can be easily integrated into any project.

By understanding and utilizing this debounce function, you can significantly improve the performance and user experience of your web applications.


1
Subscribe to my newsletter

Read articles from Dhruv Khara directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dhruv Khara
Dhruv Khara