-
-
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); |
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>
Nice variant without "any" usage. I would also replace NodeJS.Timeout with ReturnType to support both NodeJS and browser contexts.