Created
August 29, 2025 17:24
-
-
Save fiuzagr/7adda88671e90944ca4d0f72f59e24d4 to your computer and use it in GitHub Desktop.
Debounce
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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