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,801 @@
1
+ import { Optional, pipe } from '../functional/index.mjs';
2
+ import { asUint32 } from '../number/index.mjs';
3
+ import { tp } from '../others/index.mjs';
4
+
5
+ /**
6
+ * Interface for an immutable map with custom key mapping and O(1) lookup performance.
7
+ *
8
+ * IMapMapped allows you to use complex objects as keys by providing transformation functions
9
+ * that convert between your custom key type `K` and a primitive `MapSetKeyType` `KM` that can
10
+ * be efficiently stored in JavaScript's native Map. This enables high-performance operations
11
+ * on maps with complex keys while maintaining type safety and immutability.
12
+ *
13
+ * **Key Features:**
14
+ * - **Custom Key Types**: Use any type as keys by providing `toKey`/`fromKey` functions
15
+ * - **O(1) Performance**: Maintains O(1) average-case performance for core operations
16
+ * - **Immutable**: All operations return new instances, preserving immutability
17
+ * - **Type Safe**: Full TypeScript support with generic key/value types
18
+ *
19
+ * **Performance Characteristics:**
20
+ * - get/has/delete: O(1) average case (plus key transformation overhead)
21
+ * - set: O(1) average case (plus key transformation overhead)
22
+ * - map/filter operations: O(n)
23
+ * - Iteration: O(n) (plus key transformation overhead)
24
+ *
25
+ * @template K The type of the custom keys in the map.
26
+ * @template V The type of the values in the map.
27
+ * @template KM The type of the mapped primitive keys (string, number, etc.).
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // Example with complex object keys
32
+ * type UserId = { department: string; employeeId: number };
33
+ *
34
+ * // Define transformation functions
35
+ * const userIdToKey = (id: UserId): string => `${id.department}:${id.employeeId}`;
36
+ * const keyToUserId = (key: string): UserId => {
37
+ * const [department, employeeId] = key.split(':');
38
+ * return { department, employeeId: Number(employeeId) };
39
+ * };
40
+ *
41
+ * declare const userMap: IMapMapped<UserId, UserProfile, string>;
42
+ *
43
+ * // All operations work with the complex key type
44
+ * const userId: UserId = { department: "engineering", employeeId: 123 };
45
+ * const hasUser = userMap.has(userId); // O(1)
46
+ * const profile = userMap.get(userId).unwrapOr(defaultProfile); // O(1)
47
+ * const updated = userMap.set(userId, newProfile); // O(1) - returns new IMapMapped
48
+ * ```
49
+ */
50
+ type IMapMappedInterface<K, V, KM extends MapSetKeyType> = Readonly<{
51
+ // Getting information
52
+
53
+ /** The number of elements in the map. */
54
+ size: SizeType.Arr;
55
+
56
+ /**
57
+ * Checks if a key exists in the map.
58
+ * @param key The key to check.
59
+ * @returns `true` if the key exists, `false` otherwise.
60
+ */
61
+ has: (key: K) => boolean;
62
+
63
+ /**
64
+ * Retrieves the value associated with a key.
65
+ * @param key The key to retrieve.
66
+ * @returns The value associated with the key wrapped with `Optional.some`, or `Optional.none` if the key does not exist.
67
+ */
68
+ get: (key: K) => Optional<V>;
69
+
70
+ // Reducing a value
71
+
72
+ /**
73
+ * Checks if all elements in the map satisfy a predicate.
74
+ * @param predicate A function to test each key-value pair.
75
+ * @returns `true` if all elements satisfy the predicate, `false` otherwise.
76
+ */
77
+ every: ((predicate: (value: V, key: K) => boolean) => boolean) &
78
+ /**
79
+ * Checks if all elements in the map satisfy a type predicate.
80
+ * Narrows the type of values in the map if the predicate returns true for all elements.
81
+ * @template W The narrowed type of the values.
82
+ * @param predicate A type predicate function.
83
+ * @returns `true` if all elements satisfy the predicate, `false` otherwise.
84
+ */
85
+ (<W extends V>(
86
+ predicate: (value: V, key: K) => value is W,
87
+ ) => this is IMapMapped<K, W, KM>);
88
+
89
+ /**
90
+ * Checks if at least one element in the map satisfies a predicate.
91
+ * @param predicate A function to test each key-value pair.
92
+ * @returns `true` if at least one element satisfies the predicate, `false` otherwise.
93
+ */
94
+ some: (predicate: (value: V, key: K) => boolean) => boolean;
95
+
96
+ // Mutation
97
+ /**
98
+ * Deletes a key-value pair from the map.
99
+ * @param key The key to delete.
100
+ * @returns A new IMapMapped instance without the specified key.
101
+ */
102
+ delete: (key: K) => IMapMapped<K, V, KM>;
103
+
104
+ /**
105
+ * Sets a key-value pair in the map.
106
+ * @param key The key to set.
107
+ * @param value The value to associate with the key.
108
+ * @returns A new IMapMapped instance with the specified key-value pair.
109
+ */
110
+ set: (key: K, value: V) => IMapMapped<K, V, KM>;
111
+
112
+ /**
113
+ * Updates the value associated with a key using an updater function.
114
+ * @param key The key whose value to update.
115
+ * @param updater A function that takes the current value and returns the new value.
116
+ * @returns A new IMapMapped instance with the updated value.
117
+ */
118
+ update: (key: K, updater: (value: V) => V) => IMapMapped<K, V, KM>;
119
+
120
+ /**
121
+ * Applies a series of mutations to the map.
122
+ * @param actions An array of mutation actions (delete, set, or update).
123
+ * @returns A new IMapMapped instance with all mutations applied.
124
+ */
125
+ withMutations: (
126
+ actions: readonly Readonly<
127
+ | { type: 'delete'; key: K }
128
+ | { type: 'set'; key: K; value: V }
129
+ | { type: 'update'; key: K; updater: (value: V) => V }
130
+ >[],
131
+ ) => IMapMapped<K, V, KM>;
132
+
133
+ // Sequence algorithms
134
+
135
+ /**
136
+ * Maps the values of the map to new values.
137
+ * @template V2 The type of the new values.
138
+ * @param mapFn A function that maps a value and key to a new value.
139
+ * @returns A new IMapMapped instance with mapped values.
140
+ */
141
+ map: <V2>(mapFn: (value: V, key: K) => V2) => IMapMapped<K, V2, KM>;
142
+
143
+ /**
144
+ * Maps the keys of the map to new keys.
145
+ * Note: The key type cannot be changed because `toKey` and `fromKey` would become unusable.
146
+ * @param mapFn A function that maps a key to a new key of the same type.
147
+ * @returns A new IMapMapped instance with mapped keys.
148
+ */
149
+ mapKeys: (mapFn: (key: K) => K) => IMapMapped<K, V, KM>;
150
+
151
+ /**
152
+ * Maps the entries (key-value pairs) of the map to new entries.
153
+ * @template V2 The type of the new values in the entries.
154
+ * @param mapFn A function that maps an entry to a new entry (key must remain the same type).
155
+ * @returns A new IMapMapped instance with mapped entries.
156
+ */
157
+ mapEntries: <V2>(
158
+ mapFn: (entry: readonly [K, V]) => readonly [K, V2],
159
+ ) => IMapMapped<K, V2, KM>;
160
+
161
+ // Side effects
162
+
163
+ /**
164
+ * Executes a callback function for each key-value pair in the map.
165
+ * @param callbackfn A function to execute for each element.
166
+ */
167
+ forEach: (callbackfn: (value: V, key: K) => void) => void;
168
+
169
+ // Iterators
170
+
171
+ /**
172
+ * Returns an iterator for the keys in the map.
173
+ * @returns An iterable iterator of keys.
174
+ */
175
+ keys: () => IterableIterator<K>;
176
+
177
+ /**
178
+ * Returns an iterator for the values in the map.
179
+ * @returns An iterable iterator of values.
180
+ */
181
+ values: () => IterableIterator<V>;
182
+
183
+ /**
184
+ * Returns an iterator for the entries (key-value pairs) in the map.
185
+ * @returns An iterable iterator of entries.
186
+ */
187
+ entries: () => IterableIterator<readonly [K, V]>;
188
+
189
+ // Conversion
190
+
191
+ /**
192
+ * Converts the keys of the map to an array.
193
+ * @returns A readonly array of keys.
194
+ */
195
+ toKeysArray: () => readonly K[];
196
+
197
+ /**
198
+ * Converts the values of the map to an array.
199
+ * @returns A readonly array of values.
200
+ */
201
+ toValuesArray: () => readonly V[];
202
+
203
+ /**
204
+ * Converts the entries (key-value pairs) of the map to an array.
205
+ * @returns A readonly array of entries.
206
+ */
207
+ toEntriesArray: () => readonly (readonly [K, V])[];
208
+
209
+ /**
210
+ * Converts the map to an array of entries (key-value pairs).
211
+ * Alias for `toEntriesArray`.
212
+ * @returns A readonly array of entries.
213
+ */
214
+ toArray: () => readonly (readonly [K, V])[];
215
+
216
+ /**
217
+ * Returns the underlying readonly JavaScript Map.
218
+ * @returns The raw ReadonlyMap instance.
219
+ */
220
+ toRawMap: () => ReadonlyMap<KM, V>;
221
+ }>;
222
+
223
+ /**
224
+ * Represents an immutable map with custom key transformation and high-performance operations.
225
+ *
226
+ * IMapMapped is a specialized persistent data structure that enables using complex objects as map keys
227
+ * while maintaining the performance benefits of JavaScript's native Map. It achieves this by requiring
228
+ * bidirectional transformation functions that convert between your custom key type and a primitive type
229
+ * that can be efficiently stored and compared.
230
+ *
231
+ * **Key Features:**
232
+ * - **Complex Keys**: Use objects, arrays, or any custom type as map keys
233
+ * - **High Performance**: O(1) operations through efficient key transformation
234
+ * - **Immutable**: All mutation operations return new instances
235
+ * - **Type Safe**: Full TypeScript support with compile-time key/value type checking
236
+ * - **Bidirectional**: Maintains ability to reconstruct original keys from mapped keys
237
+ *
238
+ * **Use Cases:**
239
+ * - Maps with composite keys (e.g., coordinates, user IDs with metadata)
240
+ * - Caching with complex cache keys
241
+ * - State management where entities have multi-part identifiers
242
+ * - Performance-critical maps with non-primitive keys
243
+ *
244
+ * @template K The type of the custom keys in the map.
245
+ * @template V The type of the values in the map.
246
+ * @template KM The type of the mapped primitive keys (string, number, etc.).
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * // Example: Product catalog with composite keys
251
+ * type ProductKey = { brand: string; model: string; year: number };
252
+ * type Product = { name: string; price: number; inStock: boolean };
253
+ *
254
+ * // Define bidirectional transformation functions
255
+ * const productKeyToString = (key: ProductKey): string =>
256
+ * `${key.brand}|${key.model}|${key.year}`;
257
+ *
258
+ * const stringToProductKey = (str: string): ProductKey => {
259
+ * const [brand, model, yearStr] = str.split('|');
260
+ * return { brand, model, year: Number(yearStr) };
261
+ * };
262
+ *
263
+ * // Create a map with complex keys
264
+ * let catalog = IMapMapped.create<ProductKey, Product, string>(
265
+ * [],
266
+ * productKeyToString,
267
+ * stringToProductKey
268
+ * );
269
+ *
270
+ * // Use complex objects as keys naturally
271
+ * const toyotaCamry2023: ProductKey = { brand: "Toyota", model: "Camry", year: 2023 };
272
+ * const hondaAccord2022: ProductKey = { brand: "Honda", model: "Accord", year: 2022 };
273
+ *
274
+ * catalog = catalog
275
+ * .set(toyotaCamry2023, { name: "Toyota Camry 2023", price: 28000, inStock: true })
276
+ * .set(hondaAccord2022, { name: "Honda Accord 2022", price: 26500, inStock: false });
277
+ *
278
+ * // All operations work with the original key type
279
+ * console.log(catalog.get(toyotaCamry2023).unwrapOr(notFound).name);
280
+ * // Output: "Toyota Camry 2023"
281
+ *
282
+ * console.log(catalog.has(hondaAccord2022)); // Output: true
283
+ * console.log(catalog.size); // Output: 2
284
+ *
285
+ * // Iteration preserves original key types
286
+ * for (const [productKey, product] of catalog) {
287
+ * console.log(`${productKey.brand} ${productKey.model} (${productKey.year}): $${product.price}`);
288
+ * }
289
+ * // Output:
290
+ * // Toyota Camry (2023): $28000
291
+ * // Honda Accord (2022): $26500
292
+ *
293
+ * // Functional transformations work seamlessly
294
+ * const discountedCatalog = catalog.map((product, key) => ({
295
+ * ...product,
296
+ * price: Math.round(product.price * 0.9) // 10% discount
297
+ * }));
298
+ * ```
299
+ */
300
+ export type IMapMapped<K, V, KM extends MapSetKeyType> = Iterable<
301
+ readonly [K, V]
302
+ > &
303
+ IMapMappedInterface<K, V, KM>;
304
+
305
+ /**
306
+ * Provides utility functions for IMapMapped.
307
+ */
308
+ export namespace IMapMapped {
309
+ /**
310
+ * Creates a new IMapMapped instance with custom key transformation functions.
311
+ *
312
+ * This factory function creates an immutable map that can use complex objects as keys
313
+ * by providing bidirectional transformation functions. The `toKey` function converts
314
+ * your custom key type to a primitive type that can be efficiently stored, while
315
+ * `fromKey` reconstructs the original key type for iteration and access.
316
+ *
317
+ * **Performance:** O(n) where n is the number of entries in the iterable.
318
+ *
319
+ * @template K The type of the custom keys.
320
+ * @template V The type of the values.
321
+ * @template KM The type of the mapped primitive keys.
322
+ * @param iterable An iterable of key-value pairs using the custom key type.
323
+ * @param toKey A function that converts a custom key `K` to a primitive key `KM`.
324
+ * This function must be deterministic and produce unique values for unique keys.
325
+ * @param fromKey A function that converts a primitive key `KM` back to the custom key `K`.
326
+ * This should be the inverse of `toKey`.
327
+ * @returns A new IMapMapped instance containing all entries from the iterable.
328
+ *
329
+ * @example
330
+ * ```typescript
331
+ * // Example 1: Geographic coordinates as keys
332
+ * type Coordinate = { lat: number; lng: number };
333
+ * type LocationInfo = { name: string; population: number };
334
+ *
335
+ * const coordToString = (coord: Coordinate): string => `${coord.lat},${coord.lng}`;
336
+ * const stringToCoord = (str: string): Coordinate => {
337
+ * const [lat, lng] = str.split(',').map(Number);
338
+ * return { lat, lng };
339
+ * };
340
+ *
341
+ * const locationMap = IMapMapped.create<Coordinate, LocationInfo, string>(
342
+ * [
343
+ * [{ lat: 40.7128, lng: -74.0060 }, { name: "New York", population: 8000000 }],
344
+ * [{ lat: 34.0522, lng: -118.2437 }, { name: "Los Angeles", population: 4000000 }]
345
+ * ],
346
+ * coordToString,
347
+ * stringToCoord
348
+ * );
349
+ *
350
+ * const nyCoord = { lat: 40.7128, lng: -74.0060 };
351
+ * console.log(locationMap.get(nyCoord).unwrap().name); // Output: "New York"
352
+ *
353
+ * // Example 2: Multi-part business keys
354
+ * type OrderId = { customerId: string; year: number; orderNumber: number };
355
+ *
356
+ * const orderIdToKey = (id: OrderId): string =>
357
+ * `${id.customerId}:${id.year}:${id.orderNumber}`;
358
+ *
359
+ * const keyToOrderId = (key: string): OrderId => {
360
+ * const [customerId, yearStr, orderNumStr] = key.split(':');
361
+ * return {
362
+ * customerId,
363
+ * year: Number(yearStr),
364
+ * orderNumber: Number(orderNumStr)
365
+ * };
366
+ * };
367
+ *
368
+ * const orderMap = IMapMapped.create<OrderId, Order, string>(
369
+ * [],
370
+ * orderIdToKey,
371
+ * keyToOrderId
372
+ * );
373
+ *
374
+ * // Example 3: Simple case with string keys (identity transformation)
375
+ * const simpleMap = IMapMapped.create<string, number, string>(
376
+ * [["key1", 100], ["key2", 200]],
377
+ * (s) => s, // identity function
378
+ * (s) => s // identity function
379
+ * );
380
+ *
381
+ * // Example 4: From existing data structures
382
+ * const existingEntries = new Map([
383
+ * [{ id: 1, type: "user" }, { name: "Alice", active: true }],
384
+ * [{ id: 2, type: "user" }, { name: "Bob", active: false }]
385
+ * ]);
386
+ *
387
+ * type EntityKey = { id: number; type: string };
388
+ * const entityKeyToString = (key: EntityKey): string => `${key.type}_${key.id}`;
389
+ * const stringToEntityKey = (str: string): EntityKey => {
390
+ * const [type, idStr] = str.split('_');
391
+ * return { type, id: Number(idStr) };
392
+ * };
393
+ *
394
+ * const entityMap = IMapMapped.create<EntityKey, Entity, string>(
395
+ * existingEntries,
396
+ * entityKeyToString,
397
+ * stringToEntityKey
398
+ * );
399
+ * ```
400
+ */
401
+ export const create = <K, V, KM extends MapSetKeyType>(
402
+ iterable: Iterable<readonly [K, V]>,
403
+ toKey: (a: K) => KM,
404
+ fromKey: (k: KM) => K,
405
+ ): IMapMapped<K, V, KM> =>
406
+ new IMapMappedClass<K, V, KM>(iterable, toKey, fromKey);
407
+
408
+ /**
409
+ * Checks if two IMapMapped instances are structurally equal.
410
+ *
411
+ * Two IMapMapped instances are considered equal if they have the same size and contain
412
+ * exactly the same key-value pairs. The comparison is performed on the underlying mapped
413
+ * keys and values, so the transformation functions themselves don't need to be identical.
414
+ * Values are compared using JavaScript's `===` operator.
415
+ *
416
+ * **Performance:** O(n) where n is the size of the smaller map.
417
+ *
418
+ * @template K The type of the custom keys.
419
+ * @template V The type of the values.
420
+ * @template KM The type of the mapped primitive keys.
421
+ * @param a The first IMapMapped instance to compare.
422
+ * @param b The second IMapMapped instance to compare.
423
+ * @returns `true` if the maps contain exactly the same key-value pairs, `false` otherwise.
424
+ *
425
+ * @example
426
+ * ```typescript
427
+ * // Example with coordinate keys
428
+ * type Point = { x: number; y: number };
429
+ * const pointToString = (p: Point): string => `${p.x},${p.y}`;
430
+ * const stringToPoint = (s: string): Point => {
431
+ * const [x, y] = s.split(',').map(Number);
432
+ * return { x, y };
433
+ * };
434
+ *
435
+ * const map1 = IMapMapped.create<Point, string, string>(
436
+ * [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "point2"]],
437
+ * pointToString,
438
+ * stringToPoint
439
+ * );
440
+ *
441
+ * const map2 = IMapMapped.create<Point, string, string>(
442
+ * [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "point2"]], // Same content
443
+ * pointToString,
444
+ * stringToPoint
445
+ * );
446
+ *
447
+ * const map3 = IMapMapped.create<Point, string, string>(
448
+ * [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "different"]], // Different value
449
+ * pointToString,
450
+ * stringToPoint
451
+ * );
452
+ *
453
+ * console.log(IMapMapped.equal(map1, map2)); // true
454
+ * console.log(IMapMapped.equal(map1, map3)); // false (different value)
455
+ *
456
+ * // Order doesn't matter for equality
457
+ * const map4 = IMapMapped.create<Point, string, string>(
458
+ * [[{ x: 3, y: 4 }, "point2"], [{ x: 1, y: 2 }, "point1"]], // Different order
459
+ * pointToString,
460
+ * stringToPoint
461
+ * );
462
+ *
463
+ * console.log(IMapMapped.equal(map1, map4)); // true
464
+ *
465
+ * // Different transformation functions but same logical content
466
+ * const alternativePointToString = (p: Point): string => `(${p.x},${p.y})`; // Different format
467
+ * const alternativeStringToPoint = (s: string): Point => {
468
+ * const match = s.match(/\((\d+),(\d+)\)/);
469
+ * return { x: Number(match![1]), y: Number(match![2]) };
470
+ * };
471
+ *
472
+ * const map5 = IMapMapped.create<Point, string, string>(
473
+ * [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "point2"]],
474
+ * alternativePointToString,
475
+ * alternativeStringToPoint
476
+ * );
477
+ *
478
+ * // This would be false because the underlying mapped keys are different
479
+ * // even though the logical content is the same
480
+ * console.log(IMapMapped.equal(map1, map5)); // false
481
+ *
482
+ * // Empty maps
483
+ * const empty1 = IMapMapped.create<Point, string, string>([], pointToString, stringToPoint);
484
+ * const empty2 = IMapMapped.create<Point, string, string>([], pointToString, stringToPoint);
485
+ * console.log(IMapMapped.equal(empty1, empty2)); // true
486
+ * ```
487
+ */
488
+ export const equal = <K, V, KM extends MapSetKeyType>(
489
+ a: IMapMapped<K, V, KM>,
490
+ b: IMapMapped<K, V, KM>,
491
+ ): boolean => a.size === b.size && a.every((v, k) => b.get(k) === v);
492
+ }
493
+
494
+ /**
495
+ * Internal class implementation for IMapMapped providing immutable map operations with key transformation.
496
+ *
497
+ * This class implements the IMapMapped interface by maintaining a JavaScript Map with primitive keys
498
+ * internally while exposing an API that works with custom key types. The transformation between
499
+ * custom and primitive keys is handled transparently through the provided `toKey` and `fromKey` functions.
500
+ *
501
+ * **Implementation Details:**
502
+ * - Uses ReadonlyMap<KM, V> internally where KM is the primitive key type
503
+ * - Stores transformation functions for bidirectional key conversion
504
+ * - Implements copy-on-write semantics for efficiency
505
+ * - Provides optional debug messaging for development
506
+ *
507
+ * @template K The type of the custom keys.
508
+ * @template V The type of the values.
509
+ * @template KM The type of the mapped primitive keys.
510
+ * @implements IMapMapped
511
+ * @implements Iterable
512
+ * @internal This class should not be used directly. Use IMapMapped.create() instead.
513
+ */
514
+ class IMapMappedClass<K, V, KM extends MapSetKeyType>
515
+ implements IMapMapped<K, V, KM>, Iterable<readonly [K, V]>
516
+ {
517
+ readonly #map: ReadonlyMap<KM, V>;
518
+ readonly #toKey: (a: K) => KM;
519
+ readonly #fromKey: (k: KM) => K;
520
+ readonly #showNotFoundMessage: boolean;
521
+
522
+ /**
523
+ * Constructs an IMapMappedClass instance with custom key transformation.
524
+ *
525
+ * @param iterable An iterable of key-value pairs using the custom key type K.
526
+ * @param toKey A function that converts a custom key K to a primitive key KM.
527
+ * Must be deterministic and produce unique values for unique keys.
528
+ * @param fromKey A function that converts a primitive key KM back to the custom key K.
529
+ * Should be the inverse of the toKey function.
530
+ * @param showNotFoundMessage Whether to log warning messages when operations
531
+ * are performed on non-existent keys. Useful for debugging.
532
+ * Defaults to false for production use.
533
+ * @internal Use IMapMapped.create() instead of calling this constructor directly.
534
+ */
535
+ constructor(
536
+ iterable: Iterable<readonly [K, V]>,
537
+ toKey: (a: K) => KM,
538
+ fromKey: (k: KM) => K,
539
+ showNotFoundMessage: boolean = false,
540
+ ) {
541
+ this.#map = new Map(Array.from(iterable, ([k, v]) => [toKey(k), v]));
542
+ this.#toKey = toKey;
543
+ this.#fromKey = fromKey;
544
+ this.#showNotFoundMessage = showNotFoundMessage;
545
+ }
546
+
547
+ /** @inheritdoc */
548
+ get size(): SizeType.Arr {
549
+ return asUint32(this.#map.size);
550
+ }
551
+
552
+ /** @inheritdoc */
553
+ has(key: K): boolean {
554
+ return this.#map.has(this.#toKey(key));
555
+ }
556
+
557
+ /** @inheritdoc */
558
+ get(key: K): Optional<V> {
559
+ if (!this.has(key)) return Optional.none;
560
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
561
+ return Optional.some(this.#map.get(this.#toKey(key))!);
562
+ }
563
+
564
+ /** @inheritdoc */
565
+ every<W extends V>(
566
+ predicate: (value: V, key: K) => value is W,
567
+ ): this is IMapMapped<K, W, KM>;
568
+ /** @inheritdoc */
569
+ every(predicate: (value: V, key: K) => boolean): boolean;
570
+ /** @inheritdoc */
571
+ every(predicate: (value: V, key: K) => boolean): boolean {
572
+ for (const [k, v] of this.entries()) {
573
+ if (!predicate(v, k)) return false;
574
+ }
575
+
576
+ return true;
577
+ }
578
+
579
+ /** @inheritdoc */
580
+ some(predicate: (value: V, key: K) => boolean): boolean {
581
+ for (const [k, v] of this.entries()) {
582
+ if (predicate(v, k)) return true;
583
+ }
584
+
585
+ return false;
586
+ }
587
+
588
+ /** @inheritdoc */
589
+ delete(key: K): IMapMapped<K, V, KM> {
590
+ if (!this.has(key)) {
591
+ if (this.#showNotFoundMessage) {
592
+ console.warn(
593
+ `IMapMapped.delete: key not found: ${String(this.#toKey(key))}`,
594
+ );
595
+ }
596
+ return this;
597
+ }
598
+ const keyMapped = this.#toKey(key);
599
+
600
+ return IMapMapped.create(
601
+ Array.from(this.#map)
602
+ .filter(([km]) => !Object.is(km, keyMapped))
603
+ .map(([km, v]) => tp(this.#fromKey(km), v)),
604
+ this.#toKey,
605
+ this.#fromKey,
606
+ );
607
+ }
608
+
609
+ /** @inheritdoc */
610
+ set(key: K, value: V): IMapMapped<K, V, KM> {
611
+ if (value === this.get(key)) return this; // has no changes
612
+ const keyMapped = this.#toKey(key);
613
+
614
+ if (!this.has(key)) {
615
+ return IMapMapped.create(
616
+ [...this.#map, tp(keyMapped, value)].map(([km, v]) =>
617
+ tp(this.#fromKey(km), v),
618
+ ),
619
+ this.#toKey,
620
+ this.#fromKey,
621
+ );
622
+ } else {
623
+ return IMapMapped.create(
624
+ Array.from(this.#map, ([km, v]) =>
625
+ tp(this.#fromKey(km), Object.is(km, keyMapped) ? value : v),
626
+ ),
627
+ this.#toKey,
628
+ this.#fromKey,
629
+ );
630
+ }
631
+ }
632
+
633
+ /** @inheritdoc */
634
+ update(key: K, updater: (value: V) => V): IMapMapped<K, V, KM> {
635
+ const curr = this.get(key);
636
+
637
+ if (Optional.isNone(curr)) {
638
+ if (this.#showNotFoundMessage) {
639
+ console.warn(
640
+ `IMapMapped.update: key not found: ${String(this.#toKey(key))}`,
641
+ );
642
+ }
643
+ return this;
644
+ }
645
+
646
+ const keyMapped = this.#toKey(key);
647
+
648
+ return IMapMapped.create(
649
+ Array.from(
650
+ this.#map.entries(),
651
+ (keyValue) =>
652
+ pipe(keyValue)
653
+ .map(([km, v]) =>
654
+ tp(km, Object.is(km, keyMapped) ? updater(curr.value) : v),
655
+ )
656
+ .map(([km, v]) => tp(this.#fromKey(km), v)).value,
657
+ ),
658
+ this.#toKey,
659
+ this.#fromKey,
660
+ );
661
+ }
662
+
663
+ /** @inheritdoc */
664
+ withMutations(
665
+ actions: readonly Readonly<
666
+ | { type: 'delete'; key: K }
667
+ | { type: 'set'; key: K; value: V }
668
+ | { type: 'update'; key: K; updater: (value: V) => V }
669
+ >[],
670
+ ): IMapMapped<K, V, KM> {
671
+ const mut_result = new Map<KM, V>(this.#map);
672
+
673
+ for (const action of actions) {
674
+ const key = this.#toKey(action.key);
675
+
676
+ switch (action.type) {
677
+ case 'delete':
678
+ mut_result.delete(key);
679
+ break;
680
+
681
+ case 'set':
682
+ mut_result.set(key, action.value);
683
+ break;
684
+
685
+ case 'update': {
686
+ const curr = mut_result.get(key);
687
+
688
+ if (!mut_result.has(key) || curr === undefined) {
689
+ if (this.#showNotFoundMessage) {
690
+ console.warn(
691
+ `IMapMapped.withMutations::update: key not found: ${String(key)}`,
692
+ );
693
+ }
694
+ break;
695
+ }
696
+
697
+ mut_result.set(key, action.updater(curr));
698
+
699
+ break;
700
+ }
701
+ }
702
+ }
703
+
704
+ return IMapMapped.create<K, V, KM>(
705
+ Array.from(mut_result, ([k, v]) => [this.#fromKey(k), v]),
706
+ this.#toKey,
707
+ this.#fromKey,
708
+ );
709
+ }
710
+
711
+ /** @inheritdoc */
712
+ map<V2>(mapFn: (value: V, key: K) => V2): IMapMapped<K, V2, KM> {
713
+ return IMapMapped.create(
714
+ this.toArray().map(([k, v]) => tp(k, mapFn(v, k))),
715
+ this.#toKey,
716
+ this.#fromKey,
717
+ );
718
+ }
719
+
720
+ /** @inheritdoc */
721
+ mapKeys(mapFn: (key: K) => K): IMapMapped<K, V, KM> {
722
+ return IMapMapped.create(
723
+ this.toArray().map(([k, v]) => tp(mapFn(k), v)),
724
+ this.#toKey,
725
+ this.#fromKey,
726
+ );
727
+ }
728
+
729
+ /** @inheritdoc */
730
+ mapEntries<V2>(
731
+ mapFn: (entry: readonly [K, V]) => readonly [K, V2],
732
+ ): IMapMapped<K, V2, KM> {
733
+ return IMapMapped.create(
734
+ this.toArray().map(mapFn),
735
+ this.#toKey,
736
+ this.#fromKey,
737
+ );
738
+ }
739
+
740
+ /** @inheritdoc */
741
+ forEach(callbackfn: (value: V, key: K) => void): void {
742
+ for (const [km, value] of this.#map.entries()) {
743
+ callbackfn(value, this.#fromKey(km));
744
+ }
745
+ }
746
+
747
+ /**
748
+ * @inheritdoc
749
+ */
750
+ *[Symbol.iterator](): Iterator<readonly [K, V]> {
751
+ for (const e of this.entries()) {
752
+ yield e;
753
+ }
754
+ }
755
+
756
+ /** @inheritdoc */
757
+ *keys(): IterableIterator<K> {
758
+ for (const km of this.#map.keys()) {
759
+ yield this.#fromKey(km);
760
+ }
761
+ }
762
+
763
+ /** @inheritdoc */
764
+ *values(): IterableIterator<V> {
765
+ for (const v of this.#map.values()) {
766
+ yield v;
767
+ }
768
+ }
769
+
770
+ /** @inheritdoc */
771
+ *entries(): IterableIterator<readonly [K, V]> {
772
+ for (const [km, v] of this.#map.entries()) {
773
+ yield [this.#fromKey(km), v];
774
+ }
775
+ }
776
+
777
+ /** @inheritdoc */
778
+ toKeysArray(): readonly K[] {
779
+ return Array.from(this.keys());
780
+ }
781
+
782
+ /** @inheritdoc */
783
+ toValuesArray(): readonly V[] {
784
+ return Array.from(this.values());
785
+ }
786
+
787
+ /** @inheritdoc */
788
+ toEntriesArray(): readonly (readonly [K, V])[] {
789
+ return Array.from(this.entries());
790
+ }
791
+
792
+ /** @inheritdoc */
793
+ toArray(): readonly (readonly [K, V])[] {
794
+ return Array.from(this.entries());
795
+ }
796
+
797
+ /** @inheritdoc */
798
+ toRawMap(): ReadonlyMap<KM, V> {
799
+ return this.#map;
800
+ }
801
+ }