Skip to content

Instantly share code, notes, and snippets.

@fiuzagr
Created August 29, 2025 17:24
Show Gist options
  • Select an option

  • Save fiuzagr/7adda88671e90944ca4d0f72f59e24d4 to your computer and use it in GitHub Desktop.

Select an option

Save fiuzagr/7adda88671e90944ca4d0f72f59e24d4 to your computer and use it in GitHub Desktop.
Debounce
/**
* Creates a debounced version of a function that delays its execution until after
* a specified wait time has elapsed since the last time it was invoked.
*
* Features:
* - Preserves argument and this typing.
* - Supports leading (immediate) and trailing invocation options.
* - Exposes cancel() and flush() helpers.
*
* Typical use-cases: input handlers, resize/scroll listeners, search-as-you-type.
*/
export type DebounceOptions = {
/**
* If true, call on the leading edge of the timeout.
* Default: false
*/
leading?: boolean;
/**
* If true, call on the trailing edge of the timeout.
* Default: true
*/
trailing?: boolean;
};
export type AnyFunc = (...args: never[]) => unknown;
export type Debounced<T extends AnyFunc> = ((
...args: Parameters<T>
) => void) & { cancel: () => void; flush: () => void };
/**
* Debounce a function.
*
* @param fn The function to debounce.
* @param wait Wait time in milliseconds.
* @param options Debounce options.
* @returns A debounced function with cancel() and flush() helpers.
*/
export function debounce<T extends AnyFunc>(
fn: T,
wait: number,
options: DebounceOptions = {},
): Debounced<T> {
const { leading = false, trailing = true } = options;
// Use number for browser and NodeJS.Timeout for Node: we keep it compatible via `any`.
let timer: ReturnType<typeof setTimeout> | null = null;
let lastArgs: Parameters<T> | null = null;
let lastThis: unknown;
let leadingCalled = false;
const clearTimer = () => {
if (timer !== null) {
clearTimeout(timer as ReturnType<typeof setTimeout>);
timer = null;
}
};
const invoke = () => {
if (lastArgs) {
fn.apply(lastThis, lastArgs);
lastArgs = null;
leadingCalled = false;
}
};
const startTimer = () => {
timer = setTimeout(() => {
timer = null;
if (trailing) {
invoke();
} else {
// if trailing is false, reset flags/args
lastArgs = null;
leadingCalled = false;
}
}, wait) as unknown as ReturnType<typeof setTimeout>;
};
const debounced = function (this: unknown, ...args: Parameters<T>) {
// Capture calling context without aliasing `this` variable
lastThis = (function getThis(ctx: unknown) {
return ctx;
})(this);
lastArgs = args;
if (timer === null) {
if (leading && !leadingCalled) {
leadingCalled = true;
// Immediate call on the leading edge
fn.apply(lastThis, lastArgs);
lastArgs = null; // consumed
}
startTimer();
} else {
// Restart the timer on later calls within a wait window
clearTimer();
startTimer();
}
} as Debounced<T>;
debounced.cancel = () => {
clearTimer();
lastArgs = null;
leadingCalled = false;
};
debounced.flush = () => {
if (timer !== null) {
clearTimer();
if (trailing) {
invoke();
} else {
lastArgs = null;
leadingCalled = false;
}
}
};
return debounced;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment