14

I'm trying to write a debounce function with typescript.

I found an example of it in here. Code follows:

export function debounce<Params extends any[]>(
  func: (...args: Params) => any,
  timeout: number,
): (...args: Params) => void {
  let timer: NodeJS.Timeout
  return (...args: Params) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      func(...args)
    }, timeout)
  }
}

Problem is:

  • Function passed as a parameter is not getting called after the specified timeout
  • I can't use lodash or any other external library because I'm trying to avoid adding new dependencies to this project.

Thanks.

Lucas Meine
  • 1,524
  • 4
  • 23
  • 32
  • You have to use `debounce(...)();` because that function returns a function which still has to be called. –  Nov 29 '19 at 12:13
  • debounce returns a function, you might want to assign and call it, `let debounced = debounce(() => {}, 1000); debounced();` – shrys Nov 29 '19 at 12:13
  • 1
    @Fluffremovalservice I think it works well enough for what it is (see the answer below), but I'm used to debounce having an immediate option, and those typings look...like the simplest thing that could possibly work rather than something that would actually be a production implementation. – Jared Smith Nov 29 '19 at 12:25

3 Answers3

17

How do you use your debounce function? I prepare fiddle, you can check working solution here

function debounce<Params extends any[]>(
  func: (...args: Params) => any,
  timeout: number,
): (...args: Params) => void {
  let timer: NodeJS.Timeout
  return (...args: Params) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      func(...args)
    }, timeout)
  }
}

function test(message) {
  alert(message);
}

const debouncedTest = debounce(test, 2000);

debouncedTest('message');

Well, it's not typescript troubles

svltmccc
  • 1,356
  • 9
  • 25
4

This is intended as a supplement to Saveli Tomac's excellent answer.

In the comments I said I didn't think that implementation was particularly good. In particular it has two problems:

  1. It doesn't have an immediate option. Most debounce implementations in the wild (including the one you linked in your question) have this.
  2. The returned function ignores the this value.

Here's an example that fixes these:

const debounce = (n: number, fn: (...params: any[]) => any, immed: boolean = false) => {
  let timer: number | undefined = undefined;
  return function (this: any, ...args: any[]) {
    if (timer === undefined && immed) {
      fn.apply(this, args);
    }
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), n);
    return timer;
  }
};

Typescript Playground

Jared Smith
  • 19,721
  • 5
  • 45
  • 83
  • 1
    Actually, I'm getting Type 'Timeout' is not assignable to type 'number' with your example. – Lucas Meine Nov 29 '19 at 12:48
  • 1
    @Fluffremovalservice setTimeout from browser returns number (timeout id), but you call it from nodejs. You can change type for timer to NodeJS.Timeout | undefined – svltmccc Nov 29 '19 at 13:07
  • 1
    Tried it, and got Type 'Timeout' is not assignable to type 'number' in the clearTimeout(timer) line. Am I still missing something? – Lucas Meine Nov 29 '19 at 13:12
  • @Fluffremovalservice I'm not getting error in the playground, see the updated link. What version of Typescript? – Jared Smith Nov 29 '19 at 14:28
  • 1
    @Fluffremovalservice try call global.clearTimeout instead of clearTimeout – svltmccc Nov 29 '19 at 14:55
  • @JaredSmith you run your code in browser environment and it's normal behaviour – svltmccc Nov 29 '19 at 14:57
0

If the result of the function is useful for you, you can try the extension methods that I wrote for the function interface:

https://gist.github.com/falahati/fda618a9b59bb7d7f33b9ba0d5ef01a3

Usage is as simple as creating a debounce or throttled version of your function by using the trailingDebounce(wait: number), leadingDebounce(wait: number), trailingThrottle(wait: number) or the leadingThrottle(wait: number) function. Here is an example:


class RelativeOffsetCalculator {
    public addOffsetTrailingDebounce = this.addOffset.trailingDebounce(500);
    public addOffsetLeadingDebounce = this.addOffset.leadingDebounce(500);
    public addOffsetTrailingThrottle = this.addOffset.trailingThrottle(500);
    public addOffsetLeadingThrottle = this.addOffset.leadingThrottle(500);

    private _offset: number;

    constructor(offset: number) {
        this._offset = offset;
    }

    public addOffset(base: number): number {
        return base + this._offset;
    }
}

const instance = new RelativeOffsetCalculator(1);
let executions = 0;

// Call each 100ms for 10 times at a total of a second, should get limited
const intervalTimer = window.setInterval(
    () => {
        if (executions >= 10) {
            window.clearInterval(intervalTimer);
            return;
        }

        instance.addOffsetLeadingDebounce(executions).then(
            (result) => console.log(result),
            (error) => console.warn(error),
        );
        executions++;
    },
    100,
);

// A later call at 2 seconds mark, should not get limited
window.setTimeout(
    () => {
        instance.addOffsetLeadingDebounce(100).then(
            (result) => console.log("Late Execution: ", result),
            (error) => console.warn("Late Execution: ", error),
        );
    },
    (10 * 100) + 1000,
);

This results in:

1 1 1 1 1 1 1 1 1 1 Late Execution: 101,

If the addOffsetTrailingDebounce function is used, the results are:

10 10 10 10 10 10 10 10 10 10 Late Execution: 101

and if the addOffsetLeadingThrottle function is used, the results are:

1 1 1 1 1 5 5 5 5 5 Late Execution: 101

and if the addOffsetTraillingThrottle function is used, the results are:

5 5 5 5 5 10 10 10 10 10 Late Execution: 101

Soroush Falahati
  • 2,196
  • 1
  • 27
  • 38