Skip to content

Instantly share code, notes, and snippets.

@falahati
Created August 11, 2020 16:53
Show Gist options
  • Save falahati/fda618a9b59bb7d7f33b9ba0d5ef01a3 to your computer and use it in GitHub Desktop.
Save falahati/fda618a9b59bb7d7f33b9ba0d5ef01a3 to your computer and use it in GitHub Desktop.
Typescript type-safe debounce and throttle extension methods
export { };
type GenericFunction = (...params: any[]) => any;
type PromisifiedFunction<T extends GenericFunction> = (...params: Parameters<T>) => Promise<ReturnType<T>>;
declare global {
// tslint:disable-next-line:interface-name
interface Function {
leadingDebounce<T extends GenericFunction>(this: T, wait: number): PromisifiedFunction<T>;
trailingDebounce<T extends GenericFunction>(this: T, wait: number): PromisifiedFunction<T>;
leadingThrottle<T extends GenericFunction>(this: T, wait: number): PromisifiedFunction<T>;
trailingThrottle<T extends GenericFunction>(this: T, wait: number): PromisifiedFunction<T>;
}
}
const limiter = <T extends GenericFunction>(func: T, wait: number, leading: boolean, throttle: boolean): PromisifiedFunction<T> => {
let timeout: number | undefined;
// for leading
let savedResult: any;
let savedError: any;
let wasSuccessful: boolean = false;
// for trailling
let resolveCallbacks: Array<(result: any) => void> = [];
let rejectCallbacks: Array<(error: any) => void> = [];
return function(this: any) {
const context = this;
const args = arguments as any;
return new Promise(
(resolve, reject) => {
const delayedCallback = () => {
timeout = undefined;
// cleanup leading variables
savedResult = undefined;
savedError = undefined;
wasSuccessful = false;
if (!leading) {
// if trailing, execute the function now and notify previous callers
try {
const result = func.apply(context, args);
for (const callback of resolveCallbacks) {
callback(result);
}
} catch (error) {
for (const callback of rejectCallbacks) {
callback(error);
}
}
}
// cleanup trailing variables
resolveCallbacks = [];
rejectCallbacks = [];
};
if (leading && !timeout) {
// if leading and if this is the first call:
// 1. execute the function and keep the results for next calls
try {
savedResult = func.apply(context, args);
wasSuccessful = true;
resolve(savedResult);
} catch (err) {
savedError = err;
wasSuccessful = false;
reject(savedError);
}
} else if (leading) {
// if leading, but this is a duplicate call:
// 1. extend the delay if necesserly
// 2. report back the last result immediately
if (!throttle) {
window.clearTimeout(timeout);
}
if (wasSuccessful) {
resolve(savedResult);
} else {
reject(savedError);
}
} else {
// if trailing:
// 1. delay the execution if necesserly
// 2. subscribe to it's eventual result
resolveCallbacks.push(resolve);
rejectCallbacks.push(reject);
if (!throttle && timeout) {
window.clearTimeout(timeout);
}
}
// Extend or delay the execution only if:
// 1. this is not throttle limitter
// OR
// 2. when the throttle limitation already passed
if (!throttle || !timeout) {
timeout = window.setTimeout(delayedCallback, wait);
}
},
);
};
};
Function.prototype.leadingDebounce = function(this: any, wait: number) {
return limiter(this, wait, true, false);
};
Function.prototype.trailingDebounce = function(this: any, wait: number) {
return limiter(this, wait, false, false);
};
Function.prototype.leadingThrottle = function(this: any, wait: number) {
return limiter(this, wait, true, true);
};
Function.prototype.trailingThrottle = function(this: any, wait: number) {
return limiter(this, wait, false, true);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment