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.
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.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.
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:
debounceFn
, it sets a timer.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
this
test failedLife 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:
this
inside the original function was lost.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.
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);
};
}
Now we have a debounce that works:
this
contextThis 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