Utility Functions
A collection of general-purpose utility functions.
debounce
Creates a debounced function that delays invoking the provided function until after the specified wait time.
// Only executes the callback after 300ms have passed without it being called againconst debouncedSearch = debounce((query) => { console.log(`Searching for: ${query}`); // Perform search operation}, 300);
// Call this as often as neededsearchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value);});/** * Creates a debounced function that delays invoking func until after wait milliseconds * @param func - The function to debounce * @param wait - The number of milliseconds to delay * @returns Debounced function */function debounce<T extends (...args: any[]) => any>( func: T, wait: number): (...args: Parameters<T>) => void { let timeout: number | undefined;
return function(this: any, ...args: Parameters<T>) { const context = this;
clearTimeout(timeout);
timeout = window.setTimeout(() => { func.apply(context, args); }, wait); };}memoize
Creates a function that memoizes the result of a function call.
// Compute fibonacci numbers (expensive operation)const fibonacci = (n: number): number => { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2);};
// Create a memoized versionconst memoizedFib = memoize(fibonacci);
console.time('First call');memoizedFib(40); // This will take some timeconsole.timeEnd('First call');
console.time('Second call');memoizedFib(40); // This will be instantconsole.timeEnd('Second call');/** * Creates a function that memoizes the result of func * @param func - The function to have its output memoized * @returns Memoized function */function memoize<T extends (...args: any[]) => any>( func: T): (...args: Parameters<T>) => ReturnType<T> { const cache = new Map<string, ReturnType<T>>();
return function(this: any, ...args: Parameters<T>): ReturnType<T> { const key = JSON.stringify(args);
if (cache.has(key)) { return cache.get(key)!; }
const result = func.apply(this, args); cache.set(key, result);
return result; };}retry
Retries a function until it succeeds or reaches the maximum number of attempts.
// Function that might failconst fetchData = async () => { const random = Math.random(); if (random < 0.7) { throw new Error('Failed to fetch data'); } return { data: 'Success!' };};
// Retry the functionretry(fetchData, { maxAttempts: 5, delay: 1000, onRetry: (error, attempt) => { console.log(`Attempt ${attempt} failed: ${error.message}. Retrying...`); }}) .then(result => console.log('Success:', result)) .catch(error => console.error('All retries failed:', error));/** * Configuration options for the retry function */interface RetryOptions { maxAttempts: number; delay?: number; onRetry?: (error: Error, attempt: number) => void;}
/** * Retries a function until it succeeds or reaches the maximum attempts * @param fn - The function to retry * @param options - Retry configuration options * @returns Promise that resolves with the function result or rejects after all retries */async function retry<T>( fn: () => Promise<T>, options: RetryOptions): Promise<T> { const { maxAttempts, delay = 0, onRetry } = options;
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error;
if (onRetry) { onRetry(lastError, attempt); }
if (attempt < maxAttempts) { await new Promise(resolve => setTimeout(resolve, delay)); } } }
throw lastError!;}clone
Creates a shallow clone of a value.
const original = { name: 'John', age: 30, hobbies: ['reading', 'gaming'] };const cloned = clone(original);
console.log(cloned); // { name: 'John', age: 30, hobbies: ['reading', 'gaming'] }
// It's a shallow cloneconsole.log(cloned === original); // falseconsole.log(cloned.hobbies === original.hobbies); // true
// Works with various data typesclone(new Date()); // Date objectclone(new Map()); // Map objectclone(new Set()); // Set objectclone(/abc/g); // RegExp objectclone(new Int32Array([1, 2, 3])); // TypedArray/** * Creates a shallow clone of value * @param value - The value to clone * @returns A shallow clone of the value */function clone<T>(value: T): T { // Handle primitive values and functions if (value === null || typeof value !== 'object') { return value; }
// Handle Date objects if (value instanceof Date) { return new Date(value.getTime()) as unknown as T; }
// Handle RegExp objects if (value instanceof RegExp) { return new RegExp(value.source, value.flags) as unknown as T; }
// Handle Array objects if (Array.isArray(value)) { return value.slice() as unknown as T; }
// Handle Map objects if (value instanceof Map) { return new Map(value) as unknown as T; }
// Handle Set objects if (value instanceof Set) { return new Set(value) as unknown as T; }
// Handle ArrayBuffer if (value instanceof ArrayBuffer) { return value.slice(0) as unknown as T; }
// Handle TypedArrays if ( ArrayBuffer.isView(value) && !(value instanceof DataView) ) { // @ts-ignore: TypeScript doesn't know about the constructor property return new value.constructor( value.buffer.slice(0), value.byteOffset, value.length ) as unknown as T; }
// Handle plain objects return Object.assign({}, value);}cloneDeep
Creates a deep clone of a value.
const original = { name: 'John', address: { city: 'New York', country: 'USA' }, hobbies: ['reading', 'gaming']};
const cloned = cloneDeep(original);
console.log(cloned); // Deep copy of the original
// It's a deep cloneconsole.log(cloned === original); // falseconsole.log(cloned.address === original.address); // falseconsole.log(cloned.hobbies === original.hobbies); // false
// Works with nested structuresconst complex = { date: new Date(), regex: /test/g, map: new Map([['key', 'value']]), set: new Set([1, 2, 3]), array: [1, 2, { nested: true }], typedArray: new Uint8Array([1, 2, 3])};
const deepCloned = cloneDeep(complex);// All properties are deeply cloned/** * Creates a deep clone of value * @param value - The value to clone deeply * @returns A deep clone of the value */function cloneDeep<T>(value: T): T { // Handle primitive values and functions if (value === null || typeof value !== 'object') { return value; }
// Handle Date objects if (value instanceof Date) { return new Date(value.getTime()) as unknown as T; }
// Handle RegExp objects if (value instanceof RegExp) { return new RegExp(value.source, value.flags) as unknown as T; }
// Handle ArrayBuffer if (value instanceof ArrayBuffer) { return value.slice(0) as unknown as T; }
// Handle TypedArrays if ( ArrayBuffer.isView(value) && !(value instanceof DataView) ) { // @ts-ignore: TypeScript doesn't know about the constructor property return new value.constructor( value.buffer.slice(0), value.byteOffset, value.length ) as unknown as T; }
// Handle Arrays if (Array.isArray(value)) { return value.map(item => cloneDeep(item)) as unknown as T; }
// Handle Map objects if (value instanceof Map) { const clonedMap = new Map(); value.forEach((val, key) => { clonedMap.set(cloneDeep(key), cloneDeep(val)); }); return clonedMap as unknown as T; }
// Handle Set objects if (value instanceof Set) { const clonedSet = new Set(); value.forEach(val => { clonedSet.add(cloneDeep(val)); }); return clonedSet as unknown as T; }
// Handle plain objects const cloned = {} as any; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { cloned[key] = cloneDeep((value as any)[key]); } }
return cloned;}get
Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned in its place.
const object = { a: { b: { c: 3 } }, x: [1, 2, { y: 4 }]};
// Access nested properties with string pathget(object, 'a.b.c'); // => 3
// Access nested properties with array pathget(object, ['a', 'b', 'c']); // => 3
// Access array elementsget(object, 'x[2].y'); // => 4get(object, ['x', 2, 'y']); // => 4
// With default value for undefined pathsget(object, 'a.b.d', 'default'); // => 'default'get(object, 'a.x.e', 'not found'); // => 'not found'
// Works with various property namesget({ 'a-b': { c: 3 } }, ['a-b', 'c']); // => 3/** * Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned. * @param object - The object to query * @param path - The path of the property to get (array or dot-notation string) * @param defaultValue - The value returned for undefined resolved values * @returns The resolved value or defaultValue */function get<T, D = undefined>( object: any, path: string | (string | number)[], defaultValue?: D): T | D { // If object is null or not an object, return the default value if (object == null || typeof object !== 'object') { return defaultValue as D; }
// Convert string path to array const keys: (string | number)[] = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, '.$1').split('.');
// Handle empty path if (keys.length === 0) { return defaultValue as D; }
// Traverse the path let result: any = object;
for (let i = 0; i < keys.length; i++) { const key = keys[i];
if (result == null) { return defaultValue as D; }
result = result[key]; }
return result === undefined ? defaultValue as D : result;}set
Sets the value at path of object. If a portion of path doesn’t exist, it’s created. Arrays are created for missing index properties while objects are created for all other missing properties.
Note: This method mutates object.
const object = { a: [{ b: { c: 3 } }] };
// Set value with string pathset(object, 'a[0].b.c', 4);console.log(object.a[0].b.c); // => 4
// Set value with array pathset(object, ['x', 0, 'y', 'z'], 5);console.log(object.x[0].y.z); // => 5
// Creates arrays for numeric keysset({}, 'a[0].b.c', 3);// => { a: [{ b: { c: 3 } }] }
// Create objects for non-numeric keysset({}, 'a.b.c', 3);// => { a: { b: { c: 3 } } }
// Works with various property namesset({}, ['items', 0, 'name'], 'Item 1');// => { items: [{ name: 'Item 1' }] }/** * Sets the value at path of object. If a portion of path doesn't exist, it's created. * Arrays are created for missing index properties while objects are created for all other missing properties. * Note: This method mutates object. * * @param object - The object to modify * @param path - The path of the property to set (array or dot-notation string) * @param value - The value to set * @returns The modified object */function set<T extends object>( object: T, path: string | (string | number)[], value: any): T { if (object == null) { return object; }
// Convert string path to array const keys: (string | number)[] = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, '.$1').split('.');
// Handle empty path if (keys.length === 0) { return object; }
let current: any = object; const lastIndex = keys.length - 1;
for (let i = 0; i < lastIndex; i++) { const key = keys[i]; const nextKey = keys[i + 1]; const isNextKeyNumeric = typeof nextKey === 'number' || (typeof nextKey === 'string' && /^\d+$/.test(nextKey));
// If the current key doesn't exist or is not an object/array, create it if (current[key] == null) { // Create an array if the next key is numeric, otherwise create an object current[key] = isNextKeyNumeric ? [] : {}; } else if (typeof current[key] !== 'object') { // If current key exists but is not an object/array, override it current[key] = isNextKeyNumeric ? [] : {}; }
current = current[key]; }
// Set the final value current[keys[lastIndex]] = value; return object;}