Search code examples
typescriptthrottling

How to add leading as options for throttle function?


I have an utility functions in TypeScript, named throttle, that I frequently use for controlling the execution rate of functions. However, I would like to extend these functions to support leading options.

Leading:

  • When leading is set to true, the function will be executed immediately on the first call. This means the function is triggered at the beginning of the wait period. For example, if you are throttling a function on a scroll event with a leading option, the function will be called as soon as the scroll starts.

These options allow developers to fine-tune control over function execution timing, accommodating specific use cases in their applications.

Here is the current implementation of the throttle function:

export function throttle(fn: (...args: any[]) => void, wait = 100) {
    let timeoutId: ReturnType<typeof setTimeout> | undefined;
    let lastTime = Date.now();

    const execute = (...args: any[]) => {
        lastTime = Date.now();
        fn(...args);
    };

    return (...args: any[]) => {
        const currentTime = Date.now();
        const elapsed = currentTime - lastTime;

        if (elapsed >= wait) {
            // If enough time has passed since the last call, execute the function immediately
            execute(...args);
        } else {
            // If not enough time has passed, schedule the function call after the remaining delay
            if (timeoutId !== undefined) {
                clearTimeout(timeoutId);
            }

            timeoutId = setTimeout(() => {
                execute(...args);
                timeoutId = undefined;
            }, wait - elapsed);
        }
    };
}

How can I modify the throttle function to include options for leading calls?

I would appreciate any example code or explanations on how to implement these options effectively. Thank you!

The following code is for test:

const sleep = (wait: number) => new Promise((r) => setTimeout(r, wait));
(async () => {
    const wait = 100;
    const fn = ((...args: any[]) => console.log(args));
    const throttled = throttle(fn, wait);

    throttled('a', 'b', 'c'); // This should be called
    await sleep(wait - 5);
    throttled('b', 'a', 'c');
    await sleep(wait - 10);
    throttled('c', 'b', 'a'); // This should be called too

    await sleep(wait + 10); // Ensure we've waited long enough for it to be called twice
})

Solution

  • All you need to do to get leading to work is to initialise lastTime to -Infinity, so that the time elapsed on the first call of throttle from lastTime to currentTime will be Infinity and thus definitely greater than wait (assuming wait is some finite time):

    function throttle(fn: (...args: any[]) => void, wait = 100, { leading = false } = {}) {
      // ⋯ 
      let lastTime = leading ? -Infinity : Date.now();
      // ⋯ same as existing version
    }
    

    I've added leading as a destructured function parameter which defaults to false and then set the entire options object to default to the empty object. So if you call throttle(fn, wait) it will behave like throttle(fn, wait, {}), which behaves like throttle(fn, wait, {leading: false}). If you want leading to be true then you should call throttle(fn, wait, {leading: true}).

    Note also that since wait has a default argument, if you want to set options but not wait, you'll need to pass undefined in explicitly like throttle(fn, undefined, {leading: true}). Personally I'd say it's much cleaner to add wait to the options object like { wait = 100, leading = false } and then call thottle(fn, {wait: 1234}) or throttle(fn, {leading: true}). But that's a digression from the question as asked.

    Playground link to code