-
-
Save ca0v/73a31f57b397606c9813472f7493a940 to your computer and use it in GitHub Desktop.
// 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); |
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.
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);
})
};
}
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);
});
};
}
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;
stumbled over this and then found https://www.npmjs.com/package/use-debounce . works like a charm, if you are using React
The correct type for timeout
is ReturnType<typeof setTimeout>
not number
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
}
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])
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
}
}
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);
};
}
Nice variant without "any" usage. I would also replace NodeJS.Timeout with ReturnType to support both NodeJS and browser contexts.
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);
};
}, []);
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 ?
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'
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),
}
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.
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.
this
derives from the function being called, not the function parameter.- 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)
}
}
Still doesn't work:
compared to:
I think typescript can't infer function say‘ this, so try this: function say(this: {name: string,skeak: any}),
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);
};
}
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);
}
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>
.
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;
};
}
The correct type for
timeout
isReturnType<typeof setTimeout>
notnumber
The return value is a positive integer
which identifies the timer created by the call to setTimeout(). In the browser correct call is window?.setTimeout()
.
https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#return_value
Still no solution that solves my context issue
function debounce<F extends (...args: Parameters<F>) => ReturnType<F>>(
func: F,
waitFor: number,
): (...args: Parameters<F>) => void {
let timeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<F>): void => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), waitFor);
};
}
const person = {
name: 'Jack',
speak: function say(sec:number){
debounce(()=>console.log(`My name is ${this.name} , debounced ${sec}`),sec)();
}
}
person.speak(3_000);
debounce(()=>console.log('hehe'),100)();
debounce(()=>console.log('hehe 1_000'),1_000)();
person.speak(4_000);
not sure if this solution addresses the issue but works
You create a new function every time, therefore person.speak is not debounced
@sylvainpolletvillard I landed here by chance and your initial example works fine for me when I simply ignore the Typescript error commenting the lines before 'this' - not sure if that is a valid solution for you though:
// @ts-expect-error: ignore
Sure, ignoring the error works to fix the error
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 }
The timeout type should be ReturnType<typeof setTimeout>
Getting a type error in
timeout
, refactored to set the type properly.