Skip to content

Instantly share code, notes, and snippets.

@ca0v
Last active April 4, 2024 08:28
Show Gist options
  • Star 49 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ca0v/73a31f57b397606c9813472f7493a940 to your computer and use it in GitHub Desktop.
Save ca0v/73a31f57b397606c9813472f7493a940 to your computer and use it in GitHub Desktop.
Typescript Debounce
// ts 3.6x
function debounce<T extends Function>(cb: T, wait = 20) {
let h = 0;
let callable = (...args: any) => {
clearTimeout(h);
h = setTimeout(() => cb(...args), wait);
};
return <T>(<any>callable);
}
// usage
let f = debounce((a: string, b: number, c?: number) => console.log(a.length + b + c || 0));
f("hi", 1, 1);
f("world", 1);
@atkinchris
Copy link

atkinchris commented Oct 22, 2019

I'd take this a step further:

  • Type timeout as a Timeout, rather than number, and set no default value
  • Use typed parameters for the debounced function, rather than casting
  • Return a Promise, with a resolve type of the original function's return type
export const debounce = <F extends (...args: any[]) => any>(func: F, waitFor: number) => {
  let timeout

  return (...args: Parameters<F>): Promise<ReturnType<F>> =>
    new Promise(resolve => {
      if (timeout) {
        clearTimeout(timeout)
      }

      timeout = setTimeout(() => resolve(func(...args)), waitFor)
    })
}

@yix
Copy link

yix commented Dec 7, 2019

I needed a throttled function instead of a de-bounced one. Here's my take:

const throttle = <F extends (...args: any[]) => any>(func: F, waitFor: number) => {
  const now = () => new Date().getTime()
  const resetStartTime = () => startTime = now()
  let timeout: number
  let startTime: number = now() - waitFor

  return (...args: Parameters<F>): Promise<ReturnType<F>> =>
    new Promise((resolve) => {
      const timeLeft = (startTime + waitFor) - now()
      if (timeout) {
        clearTimeout(timeout)
      }
      if (startTime + waitFor <= now()) {
        resetStartTime()
        resolve(func(...args))
      } else {
        timeout = setTimeout(() => {
          resetStartTime()
          resolve(func(...args))
        }, timeLeft)
      }
    })
}

// usage
const func = (hello: string) => { console.log(new Date().getTime(), '>>>', hello) }
const thrFunc = throttle(func, 1000)
thrFunc('hello 1')
setTimeout(() => thrFunc('hello 2'), 450)
setTimeout(() => thrFunc('hello 3'), 950)
setTimeout(() => thrFunc('hello 4'), 1700)
setTimeout(() => thrFunc('hello 4.1'), 1700)
setTimeout(() => thrFunc('hello 4.2'), 1750)
setTimeout(() => thrFunc('hello 4.3'), 1995)
setTimeout(() => thrFunc('hello 4.4'), 2000)
setTimeout(() => thrFunc('hello 4.5'), 2010)
setTimeout(() => thrFunc('hello 5'), 2100)

@Shuunen
Copy link

Shuunen commented Apr 7, 2020

thanks @atkinchris working great ! 👍

@patrickhpan
Copy link

for me, @atkinchris's answer threw an error for the return type of debounced - here's my fixed version:

export const debounce = <F extends (...args: any[]) => any>(func: F, waitFor: number) => {
  let timeout: ReturnType<typeof setTimeout> | null = null;

  const debounced = (...args: Parameters<F>) => {
    if (timeout !== null) {
      clearTimeout(timeout);
      timeout = null;
    }
    timeout = setTimeout(() => func(...args), waitFor);
  };

  return debounced as (...args: Parameters<F>) => ReturnType<F>;
};

@m0dch3n
Copy link

m0dch3n commented Jul 3, 2020

for me, @atkinchris's answer threw an error for the return type of debounced - here's my fixed version:

export const debounce = <F extends (...args: any[]) => any>(func: F, waitFor: number) => {
  let timeout: ReturnType<typeof setTimeout> | null = null;

  const debounced = (...args: Parameters<F>) => {
    if (timeout !== null) {
      clearTimeout(timeout);
      timeout = null;
    }
    timeout = setTimeout(() => func(...args), waitFor);
  };

  return debounced as (...args: Parameters<F>) => ReturnType<F>;
};

For which typescript version is this working ? With TS 3.9 I get the following error:

ESLint: 
Parsing error: Unexpected token

> 1 | export const debounce = <F extends (...args: any[]) => any>(func: F, waitFor: number) => {
    |                                    ^
  2 |   let timeout: ReturnType<typeof setTimeout> | null = null;
  3 | 
  4 |   const debounced = (...args: Parameters<F>) => {

@crisu83
Copy link

crisu83 commented Dec 2, 2020

Here is a type-safe version of the debounce function we use in our project:

export const debounce = <T extends (...args: any[]) => any>(
  callback: T,
  waitFor: number
) => {
  let timeout = 0;
  return (...args: Parameters<T>): ReturnType<T> => {
    let result: any;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      result = callback(...args);
    }, waitFor);
    return result;
  };
};

Most of the examples above return undefined instead of the result of the callback function, even though their type definitions states otherwise. One should be very careful when using as to fix typing issues because sometimes it is used to circumvent some underlying problem, without addressing the actual issue.

@lloydbanks
Copy link

@crisu83 type any never guarantees a "type-safe" version

@cardeol
Copy link

cardeol commented Mar 28, 2021

Getting a type error in timeout, refactored to set the type properly.

export const debounce = <T extends (...args: any[]) => any>(
  callback: T,
  waitFor: number
) => {
  let timeout: ReturnType<typeof setTimeout>;
  return (...args: Parameters<T>): ReturnType<T> => {
    let result: any;
    timeout && clearTimeout(timeout);
    timeout = setTimeout(() => {
      result = callback(...args);
    }, waitFor);
    return result;
  };
};

@jctaoo
Copy link

jctaoo commented May 2, 2021

Getting a type error in timeout, refactored to set the type properly.

export const debounce = <T extends (...args: any[]) => any>(
  callback: T,
  waitFor: number
) => {
  let timeout: ReturnType<typeof setTimeout>;
  return (...args: Parameters<T>): ReturnType<T> => {
    let result: any;
    timeout && clearTimeout(timeout);
    timeout = setTimeout(() => {
      result = callback(...args);
    }, waitFor);
    return result;
  };
};

It has safe type but it always returns immediately. Return a Promise object will be better.

@jctaoo
Copy link

jctaoo commented May 2, 2021

Guys, this is my solution:

export function debounce<T extends (...args: any[]) => any>(
  ms: number,
  callback: T
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
  let timer: NodeJS.Timeout | undefined;

  return (...args: Parameters<T>) => {
    if (timer) {
      clearTimeout(timer);
    }
    return new Promise<ReturnType<T>>((resolve) => {
      timer = setTimeout(() => {
        const returnValue = callback(...args) as ReturnType<T>;
        resolve(returnValue);
      }, ms);
    })
  };
}

@valtism
Copy link

valtism commented Jun 25, 2021

Here's a solution I found that works for me. Inspired by the types on p-debounce, by sindresorhus.

export function debounce<T extends unknown[], U>(
  callback: (...args: T) => PromiseLike<U> | U,
  wait: number
) {
  let timer: number;

  return (...args: T): Promise<U> => {
    clearTimeout(timer);
    return new Promise((resolve) => {
      timer = setTimeout(() => resolve(callback(...args)), wait);
    });
  };
}

@aleksnick
Copy link

aleksnick commented Sep 6, 2021

import { useRef, useEffect, useCallback } from 'react';

const useDebounce = <F extends (...args: any) => any>(
  func: F,
  waitFor: number,
): ((...args: Parameters<F>) => ReturnType<F>) => {
  const timer = useRef<NodeJS.Timer | null>();
  const savedFunc = useRef<F | null>(func);

  useEffect(() => {
    savedFunc.current = func;
  }, [waitFor]);

  return useCallback((...args: any) => {
    if (timer.current) {
      clearTimeout(timer.current);
      timer.current = null;
    }

    timer.current = setTimeout(() => savedFunc.current?.(...args), waitFor);
  }, []) as (...args: Parameters<F>) => ReturnType<F>;
};

export default useDebounce;

@philicious
Copy link

stumbled over this and then found https://www.npmjs.com/package/use-debounce . works like a charm, if you are using React

@sekoyo
Copy link

sekoyo commented Mar 7, 2022

The correct type for timeout is ReturnType<typeof setTimeout> not number

@thispastwinter
Copy link

thispastwinter commented Aug 23, 2022

My 0.02. Avoids any altogether.

export const debounce = <F extends (...args: Parameters<F>) => ReturnType<F>>(
  func: F,
  waitFor: number,
) => {
  let timeout: NodeJS.Timeout

  const debounced = (...args: Parameters<F>) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => func(...args), waitFor)
  }

  return debounced
}

@thispastwinter
Copy link

thispastwinter commented Aug 23, 2022

Also, if you are using this in a React application you are probably better off wrapping your debounced function in useMemo to ensure it doesn't get recreated with every re-render (especially when calling it via an onChange event). Otherwise, wrapping it in useCallback will defeat the purpose as your function gets recreated and ultimately then gets called with every keystroke.

i.e.

const debouncedFunc = useMemo(() => {
  return debounce(yourFunc, delay)
}, [yourFunc, delay])

@ds300
Copy link

ds300 commented Sep 10, 2022

The previous versions in this thread that return a promise all suffer from the problem that the promise will never be resolved if the debounced fn is called again before the timeout fires.

here's my solution that only creates one promise per debounce window

export function debounce<T extends unknown[], U>(
  callback: (...args: T) => PromiseLike<U> | U,
  wait: number
) {
  let state:
    | undefined
    | {
        timeout: ReturnType<typeof setTimeout>
        promise: Promise<U>
        resolve: (value: U | PromiseLike<U>) => void
        reject: (value: any) => void
        latestArgs: T
      } = undefined

  return (...args: T): Promise<U> => {
    if (!state) {
      state = {} as any
      state!.promise = new Promise((resolve, reject) => {
        state!.resolve = resolve
        state!.reject = reject
      })
    }
    clearTimeout(state!.timeout)
    state!.latestArgs = args
    state!.timeout = setTimeout(() => {
      const s = state!
      state = undefined
      try {
        s.resolve(callback(...s.latestArgs))
      } catch (e) {
        s.reject(e)
      }
    }, wait)

    return state!.promise
  }
}

@dhsrocha
Copy link

My 0.02. Avoids any altogether.

export const debounce = <F extends (...args: Parameters<F>) => ReturnType<F>>(
  func: F,
  waitFor: number,
) => {
  let timeout: NodeJS.Timeout

  const debounced = (...args: Parameters<F>) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => func(...args), waitFor)
  }

  return debounced
}

A slightly tidied up version:

function debounce<F extends (...args: Parameters<F>) => ReturnType<F>>(
  func: F,
  waitFor: number,
): (...args: Parameters<F>) => void {
  let timeout: NodeJS.Timeout;
  return (...args: Parameters<F>): void => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), waitFor);
  };
}

@ethernidee
Copy link

Nice variant without "any" usage. I would also replace NodeJS.Timeout with ReturnType to support both NodeJS and browser contexts.

@manuavs
Copy link

manuavs commented Oct 20, 2022

Debounce function

const _debounce = function <T extends (...args: any[]) => void>(
    callback: T,
    debounceDelay: number = 300,
    immediate: boolean = false
) {
    let timeout: ReturnType<typeof setTimeout> | null;

    return function <U>(this: U, ...args: Parameters<typeof callback>) {
        console.log('BBB');
        const context = this;

        if (immediate && !timeout) {
            callback.apply(context, args)
        }
        if (typeof timeout === "number") {
            clearTimeout(timeout);
        }
        timeout = setTimeout(() => {
            timeout = null;
            if (!immediate) {
                callback.apply(context, args)
            }
        }, debounceDelay);
    }
}

Implementing Debounce in React


 import React, { useState, useLayoutEffect } from 'react';
 const [navBarSize, setNavBarSize] = useState<number>(10);
 
  const resizeHandlerforMainContentView = (e: Event) => {
    setNavBarSize((document.querySelector('#mainNavigationBar')?.scrollHeight || 80) - 10)
  }

  useLayoutEffect(() => {
    console.log(`onMounted App.tsx`);
    setNavBarSize((document.querySelector('#mainNavigationBar')?.scrollHeight || 80) - 10);
    const debouncedFunction = _debounce(resizeHandlerforMainContentView);

    window.addEventListener("resize", debouncedFunction);

    return () => {
      console.log(`onUnMount App.tsx`);
      window.removeEventListener('resize', debouncedFunction);
    };
  }, []);

@sylvainpolletvillard
Copy link

None of the solutions above seems to propertly infer the execution context

interface MyObj {
    method(): void
}

const obj: MyObj = {
    method: debounce(function(){
        this; // ← should be inferred as MyObj
    }, 1000)
}

Any idea how to do that ?

@joshuaaron
Copy link

None of the solutions above seems to propertly infer the execution context

interface MyObj {
    method(): void
}

const obj: MyObj = {
    method: debounce(function(){
        this; // ← should be inferred as MyObj
    }, 1000)
}

Any idea how to do that ?

Arrow functions resolve the value of this differently, as they lexically bind it to the environment in that they were created.
You need to declare the returned function as a traditional anonymous function and call the function with the correct binding of this

const debounce = <F extends (...args: Parameters<F>) => ReturnType<F>>(
  fn: F,
  delay: number,
) => {
  let timeout: ReturnType<typeof setTimeout>
  return function(...args: Parameters<F>) {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

function sayHi() {
  console.log('My name is', this.name)
}

const person = {
  name: 'Jack',
  speak: debounce(sayHi, 500),
}

person.speak() // -> 'My name is Jack'

@sylvainpolletvillard
Copy link

This doesn't solve my issue, the context is still not inferred:

const debounce = <F extends (...args: Parameters<F>) => ReturnType<F>>(
  fn: F,
  delay: number,
) => {
  let timeout: ReturnType<typeof setTimeout>
  return function(...args: Parameters<F>) {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

const person = {
  name: 'Jack',
  speak: debounce(function sayHi(){
    console.log('My name is', this.name) // 'this' implicitly has type 'any' because it does not have a type annotation.
  }, 500),
}

TypeScript playground

I've been trying to use ThisParameterType without success. I don't see how you can declare a function wrapper in TypeScript that preserves the context type.

@spence
Copy link

spence commented Jul 15, 2023

I've been trying to use ThisParameterType without success. I don't see how you can declare a function wrapper in TypeScript that preserves the context type.

  1. this derives from the function being called, not the function parameter.
  2. Arrow functions don't bind a reference to this, so convert it to a function.
function debounce<T extends (...args: Parameters<T>) => void>(this: ThisParameterType<T>, fn: T, delay = 300) {
  let timer: ReturnType<typeof setTimeout> | undefined
  return (...args: Parameters<T>) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

@sylvainpolletvillard
Copy link

Still doesn't work:

image

Playground

compared to:

image

Playground

@zhulibo
Copy link

zhulibo commented Aug 15, 2023

Still doesn't work:

image

Playground

compared to:

image

Playground

I think typescript can't infer function say‘ this, so try this: function say(this: {name: string,skeak: any}),

@yusren
Copy link

yusren commented Oct 11, 2023

What about this ?

export function debounce<T extends (...args: any[]) => any>(
  this: any,
  fn: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;

  return (...args: Parameters<T>) => {
    if (timer) {
      clearTimeout(timer); // clear any pre-existing timer
    }
    const context: any = this; // get the current context
    timer = setTimeout(() => {
      fn.apply(context, args); // call the function if time expires
    }, wait);
  };
}

@ejabu
Copy link

ejabu commented Oct 29, 2023

worked for me this one

export function debounce<T extends (...args: any[]) => any>(cb: T, wait: number) {
  let h: any;
  const callable = (...args: any) => {
      clearTimeout(h);
      h = setTimeout(() => cb(...args), wait);
  };
  return <T>(<any>callable);
}

@SSTPIERRE2
Copy link

I updated mine to use unknown instead of any because otherwise TS complains even though it doesn't matter in this particular case since we're not reassigning args <T extends (...args: unknown[]) => unknown>.

@0gust1
Copy link

0gust1 commented Feb 26, 2024

Another version,

  • it respects the types (parameters and return type) of the function to be debounced
  • it was made to be used for debouncing functions that returns something (and avoids side-effects). Result is wrapped in a promise. Consequently you'll have to await the debounced function.

The trick here (TS >=v4.6), is the generic <T extends (...args: Parameters<T>) => ReturnType<T>>:

export function debounce<T extends (...args: Parameters<T>) => ReturnType<T>>(
	callback: T,
	delay: number
) {
	let timer: ReturnType<typeof setTimeout>;
	return (...args: Parameters<T>) => {
		const p = new Promise<ReturnType<T> | Error>((resolve, reject) => {
			clearTimeout(timer);
			timer = setTimeout(() => {
				try {
					let output = callback(...args);
					resolve(output);
				} catch (err) {
					if (err instanceof Error) {
						reject(err);
					}
					reject(new Error(`An error has occurred:${err}`));
				}
			}, delay);
		});
		return p;
	};
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment