Performance optimization is crucial in modern application development. Among optimization techniques, memoization stands out as an elegant and efficient solution to significantly improve your TypeScript applications' performance. In this comprehensive guide, discover how to use this powerful technique to reduce your functions' execution time by up to 90%. 🚀
Understanding Memoization in TypeScript
Memoization is an optimization technique that involves caching function call results. Instead of recalculating the same result multiple times, we store it in memory for later reuse. This approach is particularly effective for:
- Computationally intensive functions
- Recursive operations
- API calls
- Complex data transformations
Type-Safe Memoization Implementation
Let's start by defining precise types and a safe implementation:
1// Types for memoization function
2type AnyFunction = (...args: unknown[]) => unknown;
3type MemoizedFunction<T extends AnyFunction> = T & {
4 clearCache: () => void;
5};
6
7// Type for cache entries
8interface CacheEntry<T> {
9 value: T;
10 timestamp: number;
11}
12
13// Basic memoization function with strict typing
14function memoize<T extends AnyFunction>(fn: T): MemoizedFunction<T> {
15 const cache = new Map<string, CacheEntry<ReturnType<T>>>();
16
17 const memoized = (...args: Parameters<T>): ReturnType<T> => {
18 const key = JSON.stringify(args);
19 const cached = cache.get(key);
20
21 if (cached) {
22 console.log(`Cache hit for key: ${key}`);
23 return cached.value;
24 }
25
26 const result = fn(...args);
27 cache.set(key, {
28 value: result,
29 timestamp: Date.now(),
30 });
31 console.log(`New calculation for key: ${key}`);
32 return result;
33 };
34
35 // Add method to clear cache
36 const memoizedWithClear = memoized as MemoizedFunction<T>;
37 memoizedWithClear.clearCache = () => cache.clear();
38
39 return memoizedWithClear;
40}
Advanced Implementation with Typed Configuration
1// Types for configuration options
2interface MemoizeOptions {
3 maxCacheSize: number;
4 ttl: number; // Time To Live in milliseconds
5 cacheKeyGenerator?: <T extends unknown[]>(...args: T) => string;
6}
7
8// Type for cache statistics
9interface CacheStats {
10 hits: number;
11 misses: number;
12 size: number;
13 averageAccessTime: number;
14}
15
16// Advanced implementation with complete cache management
17function advancedMemoize<T extends AnyFunction>(
18 fn: T,
19 options: Partial<MemoizeOptions> = {},
20): MemoizedFunction<T> & { getStats: () => CacheStats } {
21 const {
22 maxCacheSize = 1000,
23 ttl = Infinity,
24 cacheKeyGenerator = JSON.stringify,
25 } = options;
26
27 const cache = new Map<string, CacheEntry<ReturnType<T>>>();
28 const stats = {
29 hits: 0,
30 misses: 0,
31 totalAccessTime: 0,
32 accessCount: 0,
33 };
34
35 const memoized = (...args: Parameters<T>): ReturnType<T> => {
36 const startTime = performance.now();
37 const key = cacheKeyGenerator(args);
38 const now = Date.now();
39 const cached = cache.get(key);
40
41 const updateStats = (hit: boolean): void => {
42 const accessTime = performance.now() - startTime;
43 stats.totalAccessTime += accessTime;
44 stats.accessCount += 1;
45 if (hit) stats.hits += 1;
46 else stats.misses += 1;
47 };
48
49 // Check cache validity
50 if (cached && now - cached.timestamp <= ttl) {
51 updateStats(true);
52 return cached.value;
53 }
54
55 // Remove expired entries if necessary
56 if (cached) {
57 cache.delete(key);
58 }
59
60 // Manage cache size
61 if (cache.size >= maxCacheSize) {
62 const oldestKey = cache.keys().next().value;
63 cache.delete(oldestKey);
64 }
65
66 const result = fn(...args);
67 cache.set(key, { value: result, timestamp: now });
68 updateStats(false);
69 return result;
70 };
71
72 // Add utility methods
73 const enhanced = memoized as MemoizedFunction<T> & {
74 getStats: () => CacheStats;
75 };
76 enhanced.clearCache = () => cache.clear();
77 enhanced.getStats = () => ({
78 hits: stats.hits,
79 misses: stats.misses,
80 size: cache.size,
81 averageAccessTime:
82 stats.accessCount > 0 ? stats.totalAccessTime / stats.accessCount : 0,
83 });
84
85 return enhanced;
86}
Type-Safe Practical Examples
1. Fibonacci Sequence Calculation with Strict Types
1type FibonacciFunction = (n: number) => number;
2
3const fibMemoized = memoize<FibonacciFunction>((n) => {
4 if (n <= 1) return n;
5 return fibMemoized(n - 1) + fibMemoized(n - 2);
6});
2. Typed API Requests
1interface User {
2 id: string;
3 name: string;
4 email: string;
5}
6
7type FetchUserFunction = (userId: string) => Promise<User>;
8
9const fetchUserDataMemoized = advancedMemoize<FetchUserFunction>(
10 async (userId) => {
11 const response = await fetch(`/api/users/${userId}`);
12 if (!response.ok) {
13 throw new Error(`Failed to fetch user: ${response.statusText}`);
14 }
15 return response.json();
16 },
17 {
18 ttl: 300000, // 5-minute cache
19 maxCacheSize: 100,
20 cacheKeyGenerator: (args) => args[0], // Use ID directly as key
21 },
22);
3. Data Transformation with Validation
1interface DataTransformOptions {
2 format: 'json' | 'xml';
3 version: number;
4}
5
6type TransformFunction = (
7 data: Record<string, unknown>,
8 options: DataTransformOptions,
9) => string;
10
11const transformDataMemoized = advancedMemoize<TransformFunction>(
12 (data, options) => {
13 // Transformation logic...
14 return JSON.stringify(data);
15 },
16 {
17 cacheKeyGenerator: (args) => {
18 const [data, options] = args;
19 return `${JSON.stringify(data)}-${options.format}-${options.version}`;
20 },
21 },
22);
Type-Safe Performance Monitoring
1type MetricsWrapper<T extends AnyFunction> = T & {
2 getMetrics: () => {
3 totalCalls: number;
4 averageExecutionTime: number;
5 cacheEfficiency: number;
6 };
7};
8
9function withMetrics<T extends AnyFunction>(fn: T): MetricsWrapper<T> {
10 const metrics = {
11 calls: 0,
12 totalTime: 0,
13 cacheHits: 0,
14 };
15
16 const wrapped = (...args: Parameters<T>): ReturnType<T> => {
17 const start = performance.now();
18 const result = fn(...args);
19 metrics.totalTime += performance.now() - start;
20 metrics.calls += 1;
21
22 return result;
23 };
24
25 const enhanced = wrapped as MetricsWrapper<T>;
26 enhanced.getMetrics = () => ({
27 totalCalls: metrics.calls,
28 averageExecutionTime: metrics.totalTime / metrics.calls,
29 cacheEfficiency: metrics.cacheHits / metrics.calls,
30 });
31
32 return enhanced;
33}
Conclusion
Memoization in TypeScript becomes even more powerful when properly typed. These implementations offer:
- Complete type safety
- Compile-time error detection
- Better code maintainability
- Built-in documentation through types
Key Points to Remember
- Use strict types rather than
any
- Define clear interfaces for your configuration options
- Leverage generics for better reusability
- Implement type-safe monitoring mechanisms
The combination of memoization and TypeScript's type system allows you to optimize your applications while maintaining safe and maintainable code! 💪