The SDET Playbook

← All questions

Implement debounce and throttle in TypeScript, and test them with fake timers

Asked Sep 28, 2026Viewed 0 times

1 Answer

Sign in to answer and to vote.

  • 0
    The SDET PlaybookSep 28, 2026

    Debounce waits until calls stop for a given delay, then runs once with the last arguments (a search box waiting for the user to stop typing). Throttle runs at most once per interval, however often it's called (a scroll handler).

    export function debounce<A extends unknown[]>(fn: (...args: A) => void, waitMs: number) {
      let timer: ReturnType<typeof setTimeout> | undefined;
      return (...args: A): void => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), waitMs);
      };
    }
    
    export function throttle<A extends unknown[]>(fn: (...args: A) => void, intervalMs: number) {
      let last = -Infinity;
      return (...args: A): void => {
        const now = Date.now();
        if (now - last >= intervalMs) {
          last = now;
          fn(...args);
        }
      };
    }
    

    This throttle runs on the leading edge and drops calls inside the interval. Say so, and offer a trailing call as an option if the last event must not be lost.

    Never test these with real sleeps: the tests get slow and flaky. Fake the clock instead. Vitest's fake timers also control Date.now():

    import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
    import { debounce, throttle } from './timing';
    
    beforeEach(() => vi.useFakeTimers());
    afterEach(() => vi.useRealTimers());
    
    describe('debounce', () => {
      it('runs once, with the last arguments, after calls stop', () => {
        const fn = vi.fn();
        const debounced = debounce(fn, 300);
        debounced('a'); debounced('b');
        vi.advanceTimersByTime(299);
        expect(fn).not.toHaveBeenCalled();
        vi.advanceTimersByTime(1);
        expect(fn).toHaveBeenCalledTimes(1);
        expect(fn).toHaveBeenCalledWith('b');
      });
    });
    
    describe('throttle', () => {
      it('runs at most once per interval', () => {
        const fn = vi.fn();
        const throttled = throttle(fn, 100);
        throttled(); throttled();
        expect(fn).toHaveBeenCalledTimes(1);
        vi.advanceTimersByTime(100);
        throttled();
        expect(fn).toHaveBeenCalledTimes(2);
      });
    });
    

    The same idea works in end-to-end tests. Playwright's page.clock lets you install a fake clock in the browser and move it forward, so you can check a debounced search without waiting in real time.

    Sources: MDN: debounce, MDN: throttle, Vitest: fake timers, Playwright clock