Skip to content

Instantly share code, notes, and snippets.

@ca0v
Last active May 30, 2024 18:31
Show Gist options
  • 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);
@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;
	};
}

@oburakevych
Copy link

oburakevych commented May 9, 2024

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

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

@sylvainpolletvillard
Copy link

Still no solution that solves my context issue

@Pyotato
Copy link

Pyotato commented May 30, 2024

 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

playground

@sylvainpolletvillard
Copy link

You create a new function every time, therefore person.speak is not debounced

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