← Back to snippets
typescriptutilityperformanceevents

Throttle Function

Classic throttle utility with TypeScript generics. Supports leading and trailing invocations with cancel support.

Code

interface ThrottleOptions {
  leading?: boolean;
  trailing?: boolean;
}

function throttle<T extends (...args: unknown[]) => unknown>(
  fn: T,
  interval: number,
  options: ThrottleOptions = {}
): (...args: Parameters<T>) => void {
  const { leading = true, trailing = true } = options;

  let timeoutId: ReturnType<typeof setTimeout> | null = null;
  let lastCallTime = 0;
  let lastArgs: Parameters<T> | null = null;
  let lastThis: unknown = null;

  return function (this: unknown, ...args: Parameters<T>) {
    const now = Date.now();
    const elapsed = now - lastCallTime;

    lastArgs = args;
    lastThis = this;

    if (elapsed >= interval) {
      // Enough time has passed — invoke immediately if leading is enabled
      if (leading) {
        lastCallTime = now;
        fn.apply(this, args);
        lastArgs = null;
      }

      // Clear any pending trailing call
      if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }

      // Schedule trailing call if leading was skipped
      if (!leading && trailing) {
        timeoutId = setTimeout(() => {
          lastCallTime = Date.now();
          fn.apply(lastThis, lastArgs!);
          lastArgs = null;
          timeoutId = null;
        }, interval);
      }
    }

    // Schedule trailing invocation
    if (trailing && !timeoutId && elapsed < interval) {
      timeoutId = setTimeout(() => {
        lastCallTime = Date.now();
        if (lastArgs) fn.apply(lastThis, lastArgs);
        lastArgs = null;
        timeoutId = null;
      }, interval - elapsed);
    }
  };
}

// Usage
const throttledScroll = throttle((e: Event) => {
  console.log('Scroll position:', window.scrollY);
}, 200);

window.addEventListener('scroll', throttledScroll);

Variations

With cancel support

interface ThrottledFn<T extends (...args: unknown[]) => unknown> {
  (...args: Parameters<T>): void;
  cancel: () => void;
  flush: () => void;
}

function throttle<T extends (...args: unknown[]) => unknown>(
  fn: T,
  interval: number
): ThrottledFn<T> {
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
  let lastCallTime = 0;
  let lastArgs: Parameters<T> | null = null;
  let lastThis: unknown = null;

  const throttled = function (this: unknown, ...args: Parameters<T>) {
    const now = Date.now();
    const remaining = interval - (now - lastCallTime);

    lastArgs = args;
    lastThis = this;

    if (remaining <= 0) {
      if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      lastCallTime = now;
      fn.apply(this, args);
      lastArgs = null;
    } else if (!timeoutId) {
      timeoutId = setTimeout(() => {
        lastCallTime = Date.now();
        if (lastArgs) fn.apply(lastThis, lastArgs);
        lastArgs = null;
        timeoutId = null;
      }, remaining);
    }
  } as ThrottledFn<T>;

  throttled.cancel = () => {
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
    lastArgs = null;
    lastCallTime = 0;
  };

  throttled.flush = () => {
    if (timeoutId && lastArgs) {
      clearTimeout(timeoutId);
      timeoutId = null;
      lastCallTime = Date.now();
      fn.apply(lastThis, lastArgs);
      lastArgs = null;
    }
  };

  return throttled;
}

// Usage
const throttled = throttle(handleResize, 250);
window.addEventListener('resize', throttled);

// Later: cancel pending invocations
throttled.cancel();

// Or: immediately execute pending call
throttled.flush();