ts-data-forge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +534 -0
  3. package/package.json +101 -0
  4. package/src/array/array-utils-creation.test.mts +443 -0
  5. package/src/array/array-utils-modification.test.mts +197 -0
  6. package/src/array/array-utils-overload-type-error.test.mts +149 -0
  7. package/src/array/array-utils-reducing-value.test.mts +425 -0
  8. package/src/array/array-utils-search.test.mts +169 -0
  9. package/src/array/array-utils-set-op.test.mts +335 -0
  10. package/src/array/array-utils-slice-clamped.test.mts +113 -0
  11. package/src/array/array-utils-slicing.test.mts +316 -0
  12. package/src/array/array-utils-transformation.test.mts +790 -0
  13. package/src/array/array-utils-validation.test.mts +492 -0
  14. package/src/array/array-utils.mts +4000 -0
  15. package/src/array/array.test.mts +146 -0
  16. package/src/array/index.mts +2 -0
  17. package/src/array/tuple-utils.mts +519 -0
  18. package/src/array/tuple-utils.test.mts +518 -0
  19. package/src/collections/imap-mapped.mts +801 -0
  20. package/src/collections/imap-mapped.test.mts +860 -0
  21. package/src/collections/imap.mts +651 -0
  22. package/src/collections/imap.test.mts +932 -0
  23. package/src/collections/index.mts +6 -0
  24. package/src/collections/iset-mapped.mts +889 -0
  25. package/src/collections/iset-mapped.test.mts +1187 -0
  26. package/src/collections/iset.mts +682 -0
  27. package/src/collections/iset.test.mts +1084 -0
  28. package/src/collections/queue.mts +390 -0
  29. package/src/collections/queue.test.mts +282 -0
  30. package/src/collections/stack.mts +423 -0
  31. package/src/collections/stack.test.mts +225 -0
  32. package/src/expect-type.mts +206 -0
  33. package/src/functional/index.mts +4 -0
  34. package/src/functional/match.mts +300 -0
  35. package/src/functional/match.test.mts +177 -0
  36. package/src/functional/optional.mts +733 -0
  37. package/src/functional/optional.test.mts +619 -0
  38. package/src/functional/pipe.mts +212 -0
  39. package/src/functional/pipe.test.mts +85 -0
  40. package/src/functional/result.mts +1134 -0
  41. package/src/functional/result.test.mts +777 -0
  42. package/src/globals.d.mts +38 -0
  43. package/src/guard/has-key.mts +119 -0
  44. package/src/guard/has-key.test.mts +219 -0
  45. package/src/guard/index.mts +7 -0
  46. package/src/guard/is-non-empty-string.mts +108 -0
  47. package/src/guard/is-non-empty-string.test.mts +91 -0
  48. package/src/guard/is-non-null-object.mts +106 -0
  49. package/src/guard/is-non-null-object.test.mts +90 -0
  50. package/src/guard/is-primitive.mts +165 -0
  51. package/src/guard/is-primitive.test.mts +102 -0
  52. package/src/guard/is-record.mts +153 -0
  53. package/src/guard/is-record.test.mts +112 -0
  54. package/src/guard/is-type.mts +450 -0
  55. package/src/guard/is-type.test.mts +496 -0
  56. package/src/guard/key-is-in.mts +163 -0
  57. package/src/guard/key-is-in.test.mts +19 -0
  58. package/src/index.mts +10 -0
  59. package/src/iterator/index.mts +1 -0
  60. package/src/iterator/range.mts +120 -0
  61. package/src/iterator/range.test.mts +33 -0
  62. package/src/json/index.mts +1 -0
  63. package/src/json/json.mts +711 -0
  64. package/src/json/json.test.mts +628 -0
  65. package/src/number/branded-types/finite-number.mts +354 -0
  66. package/src/number/branded-types/finite-number.test.mts +135 -0
  67. package/src/number/branded-types/index.mts +26 -0
  68. package/src/number/branded-types/int.mts +278 -0
  69. package/src/number/branded-types/int.test.mts +140 -0
  70. package/src/number/branded-types/int16.mts +192 -0
  71. package/src/number/branded-types/int16.test.mts +170 -0
  72. package/src/number/branded-types/int32.mts +193 -0
  73. package/src/number/branded-types/int32.test.mts +170 -0
  74. package/src/number/branded-types/non-negative-finite-number.mts +223 -0
  75. package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
  76. package/src/number/branded-types/non-negative-int16.mts +187 -0
  77. package/src/number/branded-types/non-negative-int16.test.mts +201 -0
  78. package/src/number/branded-types/non-negative-int32.mts +187 -0
  79. package/src/number/branded-types/non-negative-int32.test.mts +204 -0
  80. package/src/number/branded-types/non-zero-finite-number.mts +229 -0
  81. package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
  82. package/src/number/branded-types/non-zero-int.mts +167 -0
  83. package/src/number/branded-types/non-zero-int.test.mts +177 -0
  84. package/src/number/branded-types/non-zero-int16.mts +196 -0
  85. package/src/number/branded-types/non-zero-int16.test.mts +195 -0
  86. package/src/number/branded-types/non-zero-int32.mts +196 -0
  87. package/src/number/branded-types/non-zero-int32.test.mts +197 -0
  88. package/src/number/branded-types/non-zero-safe-int.mts +196 -0
  89. package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
  90. package/src/number/branded-types/non-zero-uint16.mts +189 -0
  91. package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
  92. package/src/number/branded-types/non-zero-uint32.mts +189 -0
  93. package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
  94. package/src/number/branded-types/positive-finite-number.mts +241 -0
  95. package/src/number/branded-types/positive-finite-number.test.mts +204 -0
  96. package/src/number/branded-types/positive-int.mts +304 -0
  97. package/src/number/branded-types/positive-int.test.mts +176 -0
  98. package/src/number/branded-types/positive-int16.mts +188 -0
  99. package/src/number/branded-types/positive-int16.test.mts +197 -0
  100. package/src/number/branded-types/positive-int32.mts +188 -0
  101. package/src/number/branded-types/positive-int32.test.mts +197 -0
  102. package/src/number/branded-types/positive-safe-int.mts +187 -0
  103. package/src/number/branded-types/positive-safe-int.test.mts +210 -0
  104. package/src/number/branded-types/positive-uint16.mts +188 -0
  105. package/src/number/branded-types/positive-uint16.test.mts +203 -0
  106. package/src/number/branded-types/positive-uint32.mts +188 -0
  107. package/src/number/branded-types/positive-uint32.test.mts +203 -0
  108. package/src/number/branded-types/safe-int.mts +291 -0
  109. package/src/number/branded-types/safe-int.test.mts +170 -0
  110. package/src/number/branded-types/safe-uint.mts +187 -0
  111. package/src/number/branded-types/safe-uint.test.mts +176 -0
  112. package/src/number/branded-types/uint.mts +179 -0
  113. package/src/number/branded-types/uint.test.mts +158 -0
  114. package/src/number/branded-types/uint16.mts +186 -0
  115. package/src/number/branded-types/uint16.test.mts +170 -0
  116. package/src/number/branded-types/uint32.mts +218 -0
  117. package/src/number/branded-types/uint32.test.mts +170 -0
  118. package/src/number/enum/index.mts +2 -0
  119. package/src/number/enum/int8.mts +344 -0
  120. package/src/number/enum/int8.test.mts +180 -0
  121. package/src/number/enum/uint8.mts +293 -0
  122. package/src/number/enum/uint8.test.mts +164 -0
  123. package/src/number/index.mts +4 -0
  124. package/src/number/num.mts +604 -0
  125. package/src/number/num.test.mts +242 -0
  126. package/src/number/refined-number-utils.mts +566 -0
  127. package/src/object/index.mts +1 -0
  128. package/src/object/object.mts +447 -0
  129. package/src/object/object.test.mts +124 -0
  130. package/src/others/cast-mutable.mts +113 -0
  131. package/src/others/cast-readonly.mts +192 -0
  132. package/src/others/cast-readonly.test.mts +89 -0
  133. package/src/others/if-then.mts +98 -0
  134. package/src/others/if-then.test.mts +75 -0
  135. package/src/others/index.mts +7 -0
  136. package/src/others/map-nullable.mts +172 -0
  137. package/src/others/map-nullable.test.mts +297 -0
  138. package/src/others/memoize-function.mts +196 -0
  139. package/src/others/memoize-function.test.mts +168 -0
  140. package/src/others/tuple.mts +160 -0
  141. package/src/others/tuple.test.mts +11 -0
  142. package/src/others/unknown-to-string.mts +215 -0
  143. package/src/others/unknown-to-string.test.mts +114 -0
@@ -0,0 +1,297 @@
1
+ import { expectType } from '../expect-type.mjs';
2
+ import { pipe } from '../functional/index.mjs';
3
+ import { mapNullable } from './map-nullable.mjs';
4
+
5
+ describe('mapNullable', () => {
6
+ describe('regular usage', () => {
7
+ test('should apply function to non-null value', () => {
8
+ const result = mapNullable('hello', (s) => s.toUpperCase());
9
+ expect(result).toBe('HELLO');
10
+ expectType<typeof result, string | undefined>('=');
11
+ });
12
+
13
+ test('should apply function to non-undefined value', () => {
14
+ const result = mapNullable(42, (n) => n * 2);
15
+ expect(result).toBe(84);
16
+ expectType<typeof result, number | undefined>('=');
17
+ });
18
+
19
+ test('should return undefined for null input', () => {
20
+ const result = mapNullable(null, (s: string) => s.toUpperCase());
21
+ expect(result).toBeUndefined();
22
+ expectType<typeof result, string | undefined>('=');
23
+ });
24
+
25
+ test('should return undefined for undefined input', () => {
26
+ const result = mapNullable(undefined, (s: string) => s.toUpperCase());
27
+ expect(result).toBeUndefined();
28
+ expectType<typeof result, string | undefined>('=');
29
+ });
30
+
31
+ test('should work with complex transformations', () => {
32
+ const user = { name: 'Alice', age: 30 };
33
+ const result = mapNullable(
34
+ user,
35
+ (u) => `${u.name} is ${u.age} years old`,
36
+ );
37
+ expect(result).toBe('Alice is 30 years old');
38
+ });
39
+
40
+ test('should work with nullable object properties', () => {
41
+ const user: { name?: string } = { name: 'Bob' };
42
+ const result = mapNullable(user.name, (name) => name.toUpperCase());
43
+ expect(result).toBe('BOB');
44
+
45
+ const userWithoutName: { name?: string } = {};
46
+ const resultEmpty = mapNullable(userWithoutName.name, (name) =>
47
+ name.toUpperCase(),
48
+ );
49
+ expect(resultEmpty).toBeUndefined();
50
+ });
51
+ });
52
+
53
+ describe('curried usage', () => {
54
+ test('should support curried form with string transformation', () => {
55
+ const toUpperCase = mapNullable((s: string) => s.toUpperCase());
56
+
57
+ const result1 = toUpperCase('hello');
58
+ expect(result1).toBe('HELLO');
59
+
60
+ const result2 = toUpperCase(null);
61
+ expect(result2).toBeUndefined();
62
+
63
+ const result3 = toUpperCase(undefined);
64
+ expect(result3).toBeUndefined();
65
+ });
66
+
67
+ test('should support curried form with number transformation', () => {
68
+ const double = mapNullable((n: number) => n * 2);
69
+
70
+ const result1 = double(21);
71
+ expect(result1).toBe(42);
72
+
73
+ const result2 = double(null);
74
+ expect(result2).toBeUndefined();
75
+
76
+ const result3 = double(undefined);
77
+ expect(result3).toBeUndefined();
78
+ });
79
+
80
+ test('should support curried form with object transformation', () => {
81
+ const getName = mapNullable(
82
+ (u: Readonly<{ name: string; age: number }>) => u.name,
83
+ );
84
+
85
+ const user = { name: 'Charlie', age: 25 };
86
+ const result1 = getName(user);
87
+ expect(result1).toBe('Charlie');
88
+
89
+ const result2 = getName(null);
90
+ expect(result2).toBeUndefined();
91
+ });
92
+
93
+ test('should work with pipe composition', () => {
94
+ const toUpperCase = mapNullable((s: string) => s.toUpperCase());
95
+ const addGreeting = mapNullable((s: string) => `Hello, ${s}!`);
96
+
97
+ const result = pipe('world').map(toUpperCase).map(addGreeting).value;
98
+
99
+ expect(result).toBe('Hello, WORLD!');
100
+ });
101
+
102
+ test('should handle null values in pipe composition', () => {
103
+ const toUpperCase = mapNullable((s: string) => s.toUpperCase());
104
+ const addGreeting = mapNullable((s: string) => `Hello, ${s}!`);
105
+
106
+ const result = pipe(null as string | null)
107
+ .map(toUpperCase)
108
+ .map(addGreeting).value;
109
+
110
+ expect(result).toBeUndefined();
111
+ });
112
+
113
+ test('should work with multiple transformations in pipe', () => {
114
+ const toStr = mapNullable((n: number) => n.toString());
115
+ const addPrefix = mapNullable((s: string) => `Number: ${s}`);
116
+ const toUpperCase = mapNullable((s: string) => s.toUpperCase());
117
+
118
+ const result = pipe(42 as number | null)
119
+ .map(toStr)
120
+ .map(addPrefix)
121
+ .map(toUpperCase).value;
122
+
123
+ expect(result).toBe('NUMBER: 42');
124
+ });
125
+ });
126
+
127
+ describe('edge cases and type safety', () => {
128
+ test('should preserve type information', () => {
129
+ expectTypeOf(mapNullable('test', (s) => s.length)).toEqualTypeOf<
130
+ number | undefined
131
+ >();
132
+
133
+ expectTypeOf(mapNullable(42, (n) => n.toString())).toEqualTypeOf<
134
+ string | undefined
135
+ >();
136
+
137
+ expectTypeOf(mapNullable(true, (b) => !b)).toEqualTypeOf<
138
+ boolean | undefined
139
+ >();
140
+ });
141
+
142
+ test('should work with zero values', () => {
143
+ const result = mapNullable(0, (n) => n + 1);
144
+ expect(result).toBe(1);
145
+ });
146
+
147
+ test('should work with empty string', () => {
148
+ const result = mapNullable('', (s) => s.length);
149
+ expect(result).toBe(0);
150
+ });
151
+
152
+ test('should work with false boolean', () => {
153
+ const result = mapNullable(false, (b) => !b);
154
+ expect(result).toBe(true);
155
+ });
156
+
157
+ test('should work with arrays', () => {
158
+ const result = mapNullable([1, 2, 3], (arr) => arr.length);
159
+ expect(result).toBe(3);
160
+
161
+ const nullResult = mapNullable(
162
+ null as number[] | null,
163
+ (arr) => arr.length,
164
+ );
165
+ expect(nullResult).toBeUndefined();
166
+ });
167
+
168
+ test('should work with nested objects', () => {
169
+ const data = {
170
+ user: { profile: { name: 'Alice' } },
171
+ settings: { theme: 'dark' },
172
+ };
173
+
174
+ const result = mapNullable(data, (d) => d.user.profile.name);
175
+ expect(result).toBe('Alice');
176
+ });
177
+ });
178
+
179
+ describe('chaining operations', () => {
180
+ test('should chain multiple mapNullable operations', () => {
181
+ const getValue = (): string | null => 'hello';
182
+
183
+ const step1 = mapNullable(getValue(), (s) => s.toUpperCase());
184
+ const step2 = mapNullable(step1, (s) => s.length);
185
+ const step3 = mapNullable(step2, (n) => n * 2);
186
+
187
+ expect(step3).toBe(10); // 'HELLO'.length * 2 = 5 * 2 = 10
188
+ });
189
+
190
+ test('should short-circuit on null in chain', () => {
191
+ const getValue = (): string | null => null;
192
+
193
+ const step1 = mapNullable(getValue(), (s) => s.toUpperCase());
194
+ const step2 = mapNullable(step1, (s) => s.length);
195
+ const step3 = mapNullable(step2, (n) => n * 2);
196
+
197
+ expect(step1).toBeUndefined();
198
+ expect(step2).toBeUndefined();
199
+ expect(step3).toBeUndefined();
200
+ });
201
+
202
+ test('should work with curried functions in chain', () => {
203
+ const toUpperCase = mapNullable((s: string) => s.toUpperCase());
204
+ const getLength = mapNullable((s: string) => s.length);
205
+ const double = mapNullable((n: number) => n * 2);
206
+
207
+ const input1 = 'hello';
208
+ const result1 = double(getLength(toUpperCase(input1)));
209
+ expect(result1).toBe(10);
210
+
211
+ const input2: string | null = null;
212
+ const result2 = double(getLength(toUpperCase(input2)));
213
+ expect(result2).toBeUndefined();
214
+ });
215
+ });
216
+
217
+ describe('practical use cases', () => {
218
+ test('should work with API response handling', () => {
219
+ type ApiResponse = DeepReadonly<{
220
+ data?: {
221
+ user?: {
222
+ name?: string;
223
+ email?: string;
224
+ };
225
+ };
226
+ }>;
227
+
228
+ const response: ApiResponse = {
229
+ data: {
230
+ user: {
231
+ name: 'John Doe',
232
+ email: 'john@example.com',
233
+ },
234
+ },
235
+ };
236
+
237
+ const extractUserName = mapNullable(
238
+ (r: ApiResponse) => r.data?.user?.name,
239
+ );
240
+ const formatName = mapNullable((name: string) => `Mr. ${name}`);
241
+
242
+ const userName = extractUserName(response);
243
+ const formattedName = formatName(userName);
244
+
245
+ expect(formattedName).toBe('Mr. John Doe');
246
+ });
247
+
248
+ test('should work with form data processing', () => {
249
+ type FormData = Readonly<{
250
+ email?: string;
251
+ age?: string;
252
+ }>;
253
+
254
+ const formData: FormData = {
255
+ email: 'test@example.com',
256
+ age: '25',
257
+ };
258
+
259
+ const extractAge = mapNullable((data: FormData) => data.age);
260
+ const parseAge = mapNullable((ageStr: string) =>
261
+ Number.parseInt(ageStr, 10),
262
+ );
263
+ const validateAge = mapNullable((age: number) =>
264
+ age >= 18 ? age : null,
265
+ );
266
+
267
+ const extractedAge = extractAge(formData);
268
+ const parsedAge = parseAge(extractedAge);
269
+ const validAge = validateAge(parsedAge);
270
+
271
+ expect(validAge).toBe(25);
272
+ });
273
+
274
+ test('should handle missing data gracefully', () => {
275
+ type FormData = Readonly<{
276
+ email?: string;
277
+ age?: string;
278
+ }>;
279
+
280
+ const incompleteFormData: FormData = {
281
+ email: 'test@example.com',
282
+ // age is missing
283
+ };
284
+
285
+ const extractAge = mapNullable((data: FormData) => data.age);
286
+ const parseAge = mapNullable((ageStr: string) =>
287
+ Number.parseInt(ageStr, 10),
288
+ );
289
+
290
+ const extractedAge = extractAge(incompleteFormData);
291
+ const parsedAge = parseAge(extractedAge);
292
+
293
+ expect(extractedAge).toBeUndefined();
294
+ expect(parsedAge).toBeUndefined();
295
+ });
296
+ });
297
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Creates a memoized version of a function that caches results based on input arguments.
3
+ *
4
+ * The memoized function stores results in an internal Map and returns cached values
5
+ * for repeated calls with the same arguments. This can significantly improve performance
6
+ * for expensive computations or I/O operations.
7
+ *
8
+ * **Important considerations:**
9
+ * - The cache grows unbounded - consider memory implications for long-running applications
10
+ * - Cache keys must be primitives (string, number, boolean, symbol, null, undefined, bigint)
11
+ * - Object arguments require careful key generation to ensure uniqueness
12
+ * - Pure functions only - memoizing functions with side effects can lead to bugs
13
+ *
14
+ * @template A - The tuple type of the function arguments
15
+ * @template R - The return type of the function
16
+ * @template K - The primitive type used as the cache key (must be valid Map key)
17
+ * @param fn - The pure function to memoize
18
+ * @param argsToCacheKey - Function that converts arguments to a unique cache key
19
+ * @returns A memoized version of the input function with the same signature
20
+ *
21
+ * @example Basic memoization for expensive calculations
22
+ * ```typescript
23
+ * // Fibonacci calculation (exponential time complexity)
24
+ * const fibonacci = (n: number): number => {
25
+ * console.log(`Computing fib(${n})`);
26
+ * if (n <= 1) return n;
27
+ * return fibonacci(n - 1) + fibonacci(n - 2);
28
+ * };
29
+ *
30
+ * const memoizedFib = memoizeFunction(
31
+ * fibonacci,
32
+ * (n) => n // Number itself as key
33
+ * );
34
+ *
35
+ * memoizedFib(40); // Much faster than unmemoized version
36
+ * memoizedFib(40); // Returns instantly from cache
37
+ * ```
38
+ *
39
+ * @example Multi-argument functions with composite keys
40
+ * ```typescript
41
+ * // Grid calculation with x,y coordinates
42
+ * const calculateGridValue = (x: number, y: number, scale: number): number => {
43
+ * console.log(`Computing grid(${x},${y},${scale})`);
44
+ * // Expensive computation...
45
+ * return Math.sin(x * scale) * Math.cos(y * scale);
46
+ * };
47
+ *
48
+ * const memoizedGrid = memoizeFunction(
49
+ * calculateGridValue,
50
+ * (x, y, scale) => `${x},${y},${scale}` // String concatenation for composite key
51
+ * );
52
+ *
53
+ * // Alternative: Using bit manipulation for integer coordinates
54
+ * const memoizedGrid2 = memoizeFunction(
55
+ * calculateGridValue,
56
+ * (x, y, scale) => (x << 20) | (y << 10) | scale // Assuming small positive integers
57
+ * );
58
+ * ```
59
+ *
60
+ * @example Object arguments with selective memoization
61
+ * ```typescript
62
+ * interface User {
63
+ * id: number;
64
+ * name: string;
65
+ * email: string;
66
+ * metadata?: Record<string, unknown>;
67
+ * }
68
+ *
69
+ * const fetchUserPermissions = async (user: User): Promise<string[]> => {
70
+ * console.log(`Fetching permissions for user ${user.id}`);
71
+ * const response = await api.get(`/permissions/${user.id}`);
72
+ * return response.data;
73
+ * };
74
+ *
75
+ * // Memoize based only on user ID, ignoring other fields
76
+ * const memoizedFetchPermissions = memoizeFunction(
77
+ * fetchUserPermissions,
78
+ * (user) => user.id // Only cache by ID
79
+ * );
80
+ *
81
+ * // For multiple identifying fields
82
+ * const processUserData = (user: User, orgId: number): ProcessedData => {
83
+ * // Complex processing...
84
+ * };
85
+ *
86
+ * const memoizedProcess = memoizeFunction(
87
+ * processUserData,
88
+ * (user, orgId) => `${user.id}:${orgId}` // Composite key with separator
89
+ * );
90
+ * ```
91
+ *
92
+ * @example Memoizing recursive functions
93
+ * ```typescript
94
+ * // Recursive path finding
95
+ * const findPaths = (start: string, end: string, visited: Set<string> = new Set()): string[][] => {
96
+ * if (start === end) return [[end]];
97
+ * // ... complex recursive logic
98
+ * };
99
+ *
100
+ * // Use sorted, serialized visited set for consistent keys
101
+ * const memoizedFindPaths = memoizeFunction(
102
+ * findPaths,
103
+ * (start, end, visited = new Set()) =>
104
+ * `${start}->${end}:[${[...visited].sort().join(',')}]`
105
+ * );
106
+ * ```
107
+ *
108
+ * @example Cache key strategies
109
+ * ```typescript
110
+ * // 1. Simple primitive argument
111
+ * memoizeFunction(fn, (x: number) => x);
112
+ *
113
+ * // 2. Multiple arguments with separator
114
+ * memoizeFunction(fn, (a: string, b: number) => `${a}|${b}`);
115
+ *
116
+ * // 3. Object with specific fields
117
+ * memoizeFunction(fn, (obj: { id: number; version: number }) =>
118
+ * `${obj.id}:v${obj.version}`
119
+ * );
120
+ *
121
+ * // 4. Array argument with JSON serialization
122
+ * memoizeFunction(fn, (arr: number[]) => JSON.stringify(arr));
123
+ *
124
+ * // 5. Boolean flags as bit field
125
+ * memoizeFunction(fn, (a: boolean, b: boolean, c: boolean) =>
126
+ * (a ? 4 : 0) | (b ? 2 : 0) | (c ? 1 : 0)
127
+ * );
128
+ * ```
129
+ *
130
+ * @example Memory-conscious memoization with weak references
131
+ * ```typescript
132
+ * // For object keys, consider using WeakMap externally
133
+ * const cache = new WeakMap<object, Result>();
134
+ *
135
+ * function memoizeWithWeakMap<T extends object, R>(
136
+ * fn: (obj: T) => R
137
+ * ): (obj: T) => R {
138
+ * return (obj: T): R => {
139
+ * if (cache.has(obj)) {
140
+ * return cache.get(obj)!;
141
+ * }
142
+ * const result = fn(obj);
143
+ * cache.set(obj, result);
144
+ * return result;
145
+ * };
146
+ * }
147
+ * ```
148
+ *
149
+ * @example Anti-patterns to avoid
150
+ * ```typescript
151
+ * // ❌ Bad: Memoizing impure functions
152
+ * const memoizedRandom = memoizeFunction(
153
+ * () => Math.random(),
154
+ * () => 'key' // Always returns cached random value!
155
+ * );
156
+ *
157
+ * // ❌ Bad: Memoizing functions with side effects
158
+ * const memoizedLog = memoizeFunction(
159
+ * (msg: string) => { console.log(msg); return msg; },
160
+ * (msg) => msg // Logs only on first call!
161
+ * );
162
+ *
163
+ * // ❌ Bad: Non-unique cache keys
164
+ * const memoizedProcess = memoizeFunction(
165
+ * (user: User) => processUser(user),
166
+ * (user) => user.name // Multiple users can have same name!
167
+ * );
168
+ * ```
169
+ *
170
+ * @see https://en.wikipedia.org/wiki/Memoization
171
+ */
172
+ export const memoizeFunction = <
173
+ const A extends readonly unknown[],
174
+ R,
175
+ K extends Primitive,
176
+ >(
177
+ fn: (...args: A) => R,
178
+ argsToCacheKey: (...args: A) => K,
179
+ ): ((...args: A) => R) => {
180
+ const mut_cache = new Map<K, R>();
181
+
182
+ return (...args: A): R => {
183
+ const key = argsToCacheKey(...args);
184
+
185
+ if (mut_cache.has(key)) {
186
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
187
+ return mut_cache.get(key)!;
188
+ } else {
189
+ const result = fn(...args);
190
+
191
+ mut_cache.set(key, result);
192
+
193
+ return result;
194
+ }
195
+ };
196
+ };
@@ -0,0 +1,168 @@
1
+ /* eslint-disable @typescript-eslint/no-confusing-void-expression */
2
+ import { memoizeFunction } from './memoize-function.mjs';
3
+
4
+ describe('memoizeFunction', () => {
5
+ test('should cache results for the same arguments', () => {
6
+ const mockFn = vi.fn((x: number) => x * 2);
7
+ const memoized = memoizeFunction(mockFn, (x) => x);
8
+
9
+ // First call
10
+ expect(memoized(5)).toBe(10);
11
+ expect(mockFn).toHaveBeenCalledTimes(1);
12
+
13
+ // Second call with same argument - should use cache
14
+ expect(memoized(5)).toBe(10);
15
+ expect(mockFn).toHaveBeenCalledTimes(1);
16
+
17
+ // Call with different argument
18
+ expect(memoized(3)).toBe(6);
19
+ expect(mockFn).toHaveBeenCalledTimes(2);
20
+ });
21
+
22
+ test('should work with multiple arguments', () => {
23
+ const mockFn = vi.fn((a: number, b: number) => a + b);
24
+ const memoized = memoizeFunction(mockFn, (a, b) => `${a},${b}`);
25
+
26
+ expect(memoized(2, 3)).toBe(5);
27
+ expect(mockFn).toHaveBeenCalledTimes(1);
28
+
29
+ expect(memoized(2, 3)).toBe(5);
30
+ expect(mockFn).toHaveBeenCalledTimes(1);
31
+
32
+ expect(memoized(3, 2)).toBe(5);
33
+ expect(mockFn).toHaveBeenCalledTimes(2);
34
+ });
35
+
36
+ test('should handle functions that return undefined', () => {
37
+ const mockFn = vi.fn((_x: number) => undefined);
38
+ const memoized = memoizeFunction(mockFn, (x) => x);
39
+
40
+ expect(memoized(5)).toBe(undefined);
41
+ expect(mockFn).toHaveBeenCalledTimes(1);
42
+
43
+ // Should use cache even for undefined
44
+ expect(memoized(5)).toBe(undefined);
45
+ expect(mockFn).toHaveBeenCalledTimes(1);
46
+ });
47
+
48
+ test('should work with object arguments using primitive cache keys', () => {
49
+ type User = Readonly<{ id: number; name: string }>;
50
+ const mockFn = vi.fn((user: User) => `Hello ${user.name}`);
51
+ const memoized = memoizeFunction(mockFn, (user) => user.id);
52
+
53
+ const user1 = { id: 1, name: 'Alice' };
54
+ const user2 = { id: 1, name: 'Bob' }; // Same id, different name
55
+ const user3 = { id: 2, name: 'Charlie' };
56
+
57
+ expect(memoized(user1)).toBe('Hello Alice');
58
+ expect(mockFn).toHaveBeenCalledTimes(1);
59
+
60
+ // Same id, should use cache (even though name is different)
61
+ expect(memoized(user2)).toBe('Hello Alice');
62
+ expect(mockFn).toHaveBeenCalledTimes(1);
63
+
64
+ // Different id, should call function
65
+ expect(memoized(user3)).toBe('Hello Charlie');
66
+ expect(mockFn).toHaveBeenCalledTimes(2);
67
+ });
68
+
69
+ test('should work with different cache key types', () => {
70
+ // Number key
71
+ const withNumber = memoizeFunction(
72
+ (x: string) => x.length,
73
+ (x) => x.length,
74
+ );
75
+ expect(withNumber('hello')).toBe(5);
76
+ expect(withNumber('world')).toBe(5); // Same length, uses cache
77
+
78
+ // Boolean key
79
+ const withBoolean = memoizeFunction(
80
+ (x: number) => x * 2,
81
+ (x) => x > 0,
82
+ );
83
+ expect(withBoolean(5)).toBe(10);
84
+ expect(withBoolean(3)).toBe(10); // Both positive, uses cache
85
+ expect(withBoolean(-2)).toBe(-4); // Negative, new cache entry
86
+
87
+ // Symbol key
88
+ const sym1 = Symbol('test');
89
+ const sym2 = Symbol('test');
90
+ const withSymbol = memoizeFunction(
91
+ (_s: symbol) => Math.random(),
92
+ (s) => s,
93
+ );
94
+ const result1 = withSymbol(sym1);
95
+ expect(withSymbol(sym1)).toBe(result1); // Same symbol, uses cache
96
+ expect(withSymbol(sym2)).not.toBe(result1); // Different symbol
97
+ });
98
+
99
+ test('should handle null and undefined cache keys', () => {
100
+ const mockFn = vi.fn((x: string | null | undefined) => x ?? 'default');
101
+ const memoized = memoizeFunction(mockFn, (x) => x);
102
+
103
+ expect(memoized(null)).toBe('default');
104
+ expect(mockFn).toHaveBeenCalledTimes(1);
105
+
106
+ expect(memoized(null)).toBe('default');
107
+ expect(mockFn).toHaveBeenCalledTimes(1);
108
+
109
+ expect(memoized(undefined)).toBe('default');
110
+ expect(mockFn).toHaveBeenCalledTimes(2);
111
+
112
+ expect(memoized(undefined)).toBe('default');
113
+ expect(mockFn).toHaveBeenCalledTimes(2);
114
+ });
115
+
116
+ test('should maintain separate caches for different memoized functions', () => {
117
+ const fn1 = vi.fn((x: number) => x * 2);
118
+ const fn2 = vi.fn((x: number) => x * 3);
119
+
120
+ const memoized1 = memoizeFunction(fn1, (x) => x);
121
+ const memoized2 = memoizeFunction(fn2, (x) => x);
122
+
123
+ expect(memoized1(5)).toBe(10);
124
+ expect(memoized2(5)).toBe(15);
125
+
126
+ expect(fn1).toHaveBeenCalledTimes(1);
127
+ expect(fn2).toHaveBeenCalledTimes(1);
128
+
129
+ // Each has its own cache
130
+ expect(memoized1(5)).toBe(10);
131
+ expect(memoized2(5)).toBe(15);
132
+
133
+ expect(fn1).toHaveBeenCalledTimes(1);
134
+ expect(fn2).toHaveBeenCalledTimes(1);
135
+ });
136
+
137
+ test('should work with complex cache key generation', () => {
138
+ type Args = Readonly<{
139
+ category: string;
140
+ subcategory: string;
141
+ id: number;
142
+ }>;
143
+
144
+ const mockFn = vi.fn(
145
+ (args: Args) => `${args.category}/${args.subcategory}/${args.id}`,
146
+ );
147
+
148
+ const memoized = memoizeFunction(
149
+ mockFn,
150
+ (args) => `${args.category}:${args.subcategory}:${args.id}`,
151
+ );
152
+
153
+ const args1 = { category: 'books', subcategory: 'fiction', id: 123 };
154
+ const args2 = { category: 'books', subcategory: 'fiction', id: 123 };
155
+ const args3 = { category: 'books', subcategory: 'fiction', id: 124 };
156
+
157
+ expect(memoized(args1)).toBe('books/fiction/123');
158
+ expect(mockFn).toHaveBeenCalledTimes(1);
159
+
160
+ // Same cache key, should use cache
161
+ expect(memoized(args2)).toBe('books/fiction/123');
162
+ expect(mockFn).toHaveBeenCalledTimes(1);
163
+
164
+ // Different id, different cache key
165
+ expect(memoized(args3)).toBe('books/fiction/124');
166
+ expect(mockFn).toHaveBeenCalledTimes(2);
167
+ });
168
+ });