Generic Memoization Function
Caches function results based on arguments to avoid redundant computations
Implementation
Copy the complete example or adapt its typed interface to your project. Comments are kept inside the snippet so the intent travels with the code.
/**
* Generic memoization function
* @param fn Function to memoize
* @returns Memoized function
*/
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return function(this: ThisParameterType<T>, ...args: Parameters<T>) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
} as T;
}
// Usage Example
// Expensive computation function
const fibonacci = memoize((n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // Computed and cached
console.log(fibonacci(10)); // Returned from cache (much faster)
// With object arguments
const getUserData = memoize(async (userId: number, options: { details: boolean }) => {
const response = await fetch(`https://api.example.com/users/${userId}`);
return response.json();
});When to use it
Perfect for optimizing expensive computations, API calls, or any function with repeated identical calls
Review the input assumptions and browser or runtime support before adopting a utility unchanged. Small helpers are easiest to maintain when their contract stays narrow.
Reasoning and trade-offs
How it works
The wrapper serializes arguments into a cache key, stores the computed return value in a Map, and returns that value for later calls with the same serialized input. The generic signature preserves the original callable shape.
What to watch for
Serialization has the same limits as JSON and object property order can affect keys. The cache grows without bounds, rejected promises can remain cached, mutable arguments can change after lookup, and side-effecting functions must never be memoized.
Production use
Use memoization for deterministic, expensive calculations with a small repeatable input space. Add eviction or a WeakMap strategy when lifetimes are unbounded, and define whether async failures, undefined results, or object identity should be cached.
Before you ship
- Confirm the input contract with tests that cover empty, invalid, boundary, and representative real-world values before sharing the helper across a codebase.
- Keep the function focused on one responsibility and name any project-specific policy explicitly instead of hiding it inside a generic-looking utility.
- Measure behavior in the browsers or runtime versions your product supports, especially when timing, storage, DOM state, locale, or network access is involved.
- Document the return type, mutation behavior, failure mode, and performance characteristics beside the call site so future maintainers can evaluate changes safely.
- Compare the example with current official platform documentation when it relies on a browser API, language feature, or runtime behavior that may change over time.
- If the helper crosses a trust boundary, validate again on the server and avoid treating browser-side checks, TypeScript annotations, or formatted output as a security guarantee.
- Add a small regression test whenever a real edge case is discovered; concrete examples preserve the reason behind a guard more reliably than a general comment.