Wanna see something cool? Check out Angular Spotify 🎧

Typescript this and implement debounce

Back in 2020, I was in an interview and the interviewer asked me to implement a debounce function. I was surprised — I had been using debounce functions for years, mostly from libraries like Lodash, but I never actually had to write one from scratch. I did not solve it back then. Recently I stumbled upon the same question again, and this time I decided to finally sit down and do it.

Let go through it together — what debounce is, why it matters, and how to implement one correctly in TypeScript.


What is debounce?

The idea of debouncing is to control how frequently a function runs over a period of time. You give it a time window, and it ensures the function only gets called once after that window has passed without further calls.

For example:

const sayHello = debounce(() => {
  console.log('hello');
}, 300);

sayHello();
sayHello();
sayHello(); // Only logs once after 300ms

Debouncing makes sure sayHello() only runs after a set delay (300ms) has passed since the last time it was called.

This is useful for scenarios like:

  • mousemove events: These can fire hundreds of times per second. You do not want to handle every single one.
  • Search boxes: Instead of sending a request on every keystroke, you wait until the user stops typing for a short moment.

Example for mousemove:

document.addEventListener('mousemove', debounce((event: MouseEvent) => {
  console.log(`Mouse at (${event.clientX}, ${event.clientY})`);
}, 100));

Now the log only shows up at most once every 100ms, no matter how fast you move the mouse.


Step 1: Basic implementation

Let start with the function signature:

export default function debounce(func: Function, wait: number): Function {
}

It takes two inputs: a function to debounce and a wait time in milliseconds. It returns a new function that wraps the original.

A basic version looks like this:

export default function debounce(func: Function, wait: number): Function {
  let timer: any = null;
  function debounceFn() {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      func();
    }, wait);
  }
  return debounceFn;
}

How this works:

  • The first time you call debounceFn, it sets a timer.
  • If you call it again before the timer finishes, it clears the old timer and starts a new one.
  • The original func only gets executed once the calls stop for wait milliseconds.

Test case:

const sayHello = debounce(() => {
  console.log('hello');
}, 300);

sayHello(); // Sets a timer

setTimeout(() => {
  sayHello(); // Resets the timer after 200ms
}, 200);

// Result: 'hello' logs after 500ms total

Step 2: this test failed

Life is not that easy, one test failed 😂

test('callbacks can access `this`', (done) => {
  const increment = debounce(function (this: any, delta: number) {
    this.val += delta;
  }, 10);

  const obj = {
    val: 2,
    increment,
  };

  expect(obj.val).toBe(2);
  obj.increment(3);
  expect(obj.val).toBe(2); // still 2, not yet updated

  setTimeout(() => {
    expect(obj.val).toBe(5); // expect 2 + 3
    done();
  }, 20);
});

But the result was wrong. The value stayed at 2.

Why?

Two reasons:

  1. The this inside the original function was lost.
  2. Arguments like delta were not passed through at all.

In our earlier version:

setTimeout(() => {
  func(); // no context, no arguments
}, wait);

That func() call is missing both this and the original parameters. Time to fix both.


Step 3: Fix this and arguments using apply

To preserve this and the function parameters, I updated the debounce to use .apply.

apply lets you call a function with a specific this value and pass arguments as an array.

Also, TypeScript needs a little help to understand the use of this inside the function. You can explicitly annotate it as the first argument like this:

function (this: any, ...args: any[])

This is a TypeScript-specific feature that was introduced in version 2.0 — more details Specifying the type of this for functions.

So now the code looks like:

export default function debounce(func: Function, wait: number): Function {
  let timer: any = null;
  return function debounceFn(this: unknown, ...args: any[]) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      func.apply(this, args); // pass this and args correctly
    }, wait);
  };
}

Final Result

Now we have a debounce that works:

  • It delays execution properly
  • It preserves this context
  • It forwards all arguments

This version passed all my tests.

const increment = debounce(function (this: any, delta: number) {
  this.val += delta;
}, 10);

const obj = {
  val: 2,
  increment,
};

obj.increment(3);

// After 10ms, obj.val === 5

That was a fun exercise for sure! You can practice it here at greatfrontend.com

typescript this

Published 7 Jun 2025

    Read more

     — Classnames implementation in TypeScript
     — Validating Python Indentation in TypeScript
     — React Aria Components - Slider with filled background
     — Learn Angular Signals by implementing your own
     — Cursor: Customize Your Sidebar Like VS Code