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,447 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
2
+ /**
3
+ * A collection of type-safe object utility functions providing functional programming patterns
4
+ * for object manipulation, including pick, omit, shallow equality checks, and more.
5
+ *
6
+ * All functions maintain TypeScript type safety and support both direct and curried usage patterns
7
+ * for better composition with pipe operations.
8
+ */
9
+ export namespace Obj {
10
+ /**
11
+ * Performs a shallow equality check on two records using a configurable equality function.
12
+ * Verifies that both records have the same number of entries and that for every key in the first record,
13
+ * the corresponding value passes the equality test with the value in the second record.
14
+ *
15
+ * @param a - The first record to compare
16
+ * @param b - The second record to compare
17
+ * @param eq - Optional equality function (defaults to Object.is for strict equality)
18
+ * @returns `true` if the records are shallowly equal according to the equality function, `false` otherwise
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // Basic usage with default Object.is equality
23
+ * Obj.shallowEq({ x: 1, y: 2 }, { x: 1, y: 2 }); // true
24
+ * Obj.shallowEq({ x: 1 }, { x: 1, y: 2 }); // false (different number of keys)
25
+ * Obj.shallowEq({ x: 1 }, { x: 2 }); // false (different values)
26
+ * Obj.shallowEq({}, {}); // true (both empty)
27
+ *
28
+ * // String comparisons
29
+ * Obj.shallowEq({ a: "hello" }, { a: "hello" }); // true
30
+ * Obj.shallowEq({ a: "hello" }, { a: "world" }); // false
31
+ *
32
+ * // Using custom equality function
33
+ * const caseInsensitiveEq = (a: unknown, b: unknown) =>
34
+ * typeof a === 'string' && typeof b === 'string'
35
+ * ? a.toLowerCase() === b.toLowerCase()
36
+ * : a === b;
37
+ *
38
+ * Obj.shallowEq({ name: "ALICE" }, { name: "alice" }, caseInsensitiveEq); // true
39
+ *
40
+ * // Handling special values
41
+ * Obj.shallowEq({ x: NaN }, { x: NaN }); // true (Object.is treats NaN === NaN)
42
+ * Obj.shallowEq({ x: +0 }, { x: -0 }); // false (Object.is distinguishes +0 and -0)
43
+ * ```
44
+ */
45
+ export const shallowEq = (
46
+ a: UnknownRecord,
47
+ b: UnknownRecord,
48
+ eq: (x: unknown, y: unknown) => boolean = Object.is,
49
+ ): boolean => {
50
+ const aEntries = Object.entries(a);
51
+ const bEntries = Object.entries(b);
52
+
53
+ if (aEntries.length !== bEntries.length) return false;
54
+
55
+ return aEntries.every(([k, v]) => eq(b[k], v));
56
+ };
57
+
58
+ /**
59
+ * Creates a new record that contains only the specified keys from the source record.
60
+ * This function supports both direct usage and curried form for functional composition.
61
+ *
62
+ * **Type Safety**: Only keys that exist in the source record type are allowed,
63
+ * preventing runtime errors from accessing non-existent properties.
64
+ *
65
+ * @template R - The type of the input record
66
+ * @template Keys - The readonly array type of keys to pick from the record
67
+ *
68
+ * @param record - The source record to pick properties from
69
+ * @param keys - A readonly array of keys to include in the result
70
+ * @returns A new record containing only the specified keys and their values
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * // Direct usage
75
+ * const original = { a: 1, b: 2, c: 3, d: 4 };
76
+ * Obj.pick(original, ['a', 'c']); // { a: 1, c: 3 }
77
+ * Obj.pick(original, ['b']); // { b: 2 }
78
+ * Obj.pick(original, []); // {} (empty result)
79
+ *
80
+ * // Real-world example with user data
81
+ * const user = {
82
+ * id: 1,
83
+ * name: "Alice",
84
+ * email: "alice@example.com",
85
+ * password: "secret123",
86
+ * age: 30
87
+ * };
88
+ *
89
+ * // Extract public user information
90
+ * const publicUser = Obj.pick(user, ['id', 'name', 'email']);
91
+ * // Result: { id: 1, name: "Alice", email: "alice@example.com" }
92
+ *
93
+ * // Curried usage for functional composition
94
+ * const pickIdAndName = Obj.pick(['id', 'name'] as const);
95
+ * const users = [user, { id: 2, name: "Bob", email: "bob@example.com", age: 25 }];
96
+ * const publicUsers = users.map(pickIdAndName);
97
+ * // Result: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
98
+ *
99
+ * // Using with pipe for data transformation
100
+ * import { pipe } from '../functional/pipe.mjs';
101
+ * const result = pipe(user)
102
+ * .map(Obj.pick(['id', 'name']))
103
+ * .map(u => ({ ...u, displayName: u.name.toUpperCase() }))
104
+ * .value;
105
+ * // Result: { id: 1, name: "Alice", displayName: "ALICE" }
106
+ *
107
+ * // Type safety prevents invalid keys
108
+ * // Obj.pick(user, ['invalidKey']); // ❌ TypeScript error
109
+ * // Obj.pick(user, ['id', 'nonExistentField']); // ❌ TypeScript error
110
+ *
111
+ * // Partial key selection (when some keys might not exist)
112
+ * const partialUser = { id: 1, name: "Alice" } as const;
113
+ * const pickVisible = Obj.pick(['name', 'age']); // age might not exist
114
+ * const visible = pickVisible(partialUser); // { name: "Alice" } (age omitted)
115
+ * ```
116
+ */
117
+ export const pick: PickFnOverload = (<
118
+ const R extends UnknownRecord,
119
+ const Keys extends readonly (keyof R)[],
120
+ >(
121
+ ...args:
122
+ | readonly [record: R, keys: Keys]
123
+ | readonly [keys: readonly PropertyKey[]]
124
+ ):
125
+ | Pick<R, ArrayElement<Keys>>
126
+ | ((record: R) => RelaxedPick<R, ArrayElement<Keys>>) => {
127
+ switch (args.length) {
128
+ case 2: {
129
+ const [record, keys] = args;
130
+ const keysSet = new Set<keyof R>(keys);
131
+
132
+ return Object.fromEntries(
133
+ Object.entries(record).filter(([k, _v]) => keysSet.has(k)),
134
+ ) as never;
135
+ }
136
+
137
+ case 1: {
138
+ const [keys] = args;
139
+ return <R2 extends UnknownRecord>(record: R2) =>
140
+ pick(record, keys as readonly (keyof R2)[]);
141
+ }
142
+ }
143
+ }) as PickFnOverload;
144
+
145
+ type PickFnOverload = {
146
+ <const R extends UnknownRecord, const Keys extends readonly (keyof R)[]>(
147
+ record: R,
148
+ keys: Keys,
149
+ ): Pick<R, ArrayElement<Keys>>;
150
+
151
+ // Curried version
152
+ <const Keys extends readonly PropertyKey[]>(
153
+ keys: Keys,
154
+ ): <const R extends UnknownRecord>(
155
+ record: R,
156
+ ) => RelaxedPick<R, ArrayElement<Keys>>;
157
+ };
158
+
159
+ /**
160
+ * Creates a new record that excludes the specified keys from the source record.
161
+ * This function supports both direct usage and curried form for functional composition.
162
+ *
163
+ * **Type Safety**: Only keys that exist in the source record type are allowed,
164
+ * and the return type precisely reflects which properties remain after omission.
165
+ *
166
+ * @template R - The type of the input record
167
+ * @template Keys - The readonly array type of keys to omit from the record
168
+ *
169
+ * @param record - The source record to omit properties from
170
+ * @param keys - A readonly array of keys to exclude from the result
171
+ * @returns A new record containing all properties except the specified keys
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * // Direct usage
176
+ * const original = { a: 1, b: 2, c: 3, d: 4 };
177
+ * Obj.omit(original, ['a', 'c']); // { b: 2, d: 4 }
178
+ * Obj.omit(original, ['b']); // { a: 1, c: 3, d: 4 }
179
+ * Obj.omit(original, []); // { a: 1, b: 2, c: 3, d: 4 } (no keys omitted)
180
+ *
181
+ * // Real-world example: removing sensitive data
182
+ * const user = {
183
+ * id: 1,
184
+ * name: "Alice",
185
+ * email: "alice@example.com",
186
+ * password: "secret123",
187
+ * apiKey: "abc-def-ghi",
188
+ * lastLogin: new Date()
189
+ * };
190
+ *
191
+ * // Create safe user object for client-side
192
+ * const safeUser = Obj.omit(user, ['password', 'apiKey']);
193
+ * // Result: { id: 1, name: "Alice", email: "alice@example.com", lastLogin: Date }
194
+ *
195
+ * // Curried usage for data processing pipelines
196
+ * const removeSensitive = Obj.omit(['password', 'apiKey', 'ssn'] as const);
197
+ * const users = [user]; // array of user objects
198
+ * const safeUsers = users.map(removeSensitive);
199
+ *
200
+ * // Using with pipe for complex transformations
201
+ * import { pipe } from '../functional/pipe.mjs';
202
+ * const publicProfile = pipe(user)
203
+ * .map(Obj.omit(['password', 'apiKey']))
204
+ * .map(u => ({ ...u, displayName: u.name.toUpperCase() }))
205
+ * .value;
206
+ * // Result: { id: 1, name: "Alice", email: "...", lastLogin: Date, displayName: "ALICE" }
207
+ *
208
+ * // Database queries: exclude computed fields
209
+ * const dbUser = {
210
+ * id: 1,
211
+ * name: "Alice",
212
+ * email: "alice@example.com",
213
+ * createdAt: new Date(),
214
+ * updatedAt: new Date(),
215
+ * fullName: "Alice Johnson", // computed field
216
+ * isActive: true // computed field
217
+ * };
218
+ *
219
+ * const storableData = Obj.omit(dbUser, ['fullName', 'isActive']);
220
+ * // Only data that should be persisted to database
221
+ *
222
+ * // Type safety prevents invalid operations
223
+ * // Obj.omit(user, ['invalidKey']); // ❌ TypeScript error
224
+ * // Obj.omit(user, ['id', 'nonExistentField']); // ❌ TypeScript error
225
+ *
226
+ * // Handling partial omission (when some keys might not exist)
227
+ * const partialUser = { id: 1, name: "Alice", password: "secret" } as const;
228
+ * const omitCredentials = Obj.omit(['password', 'apiKey']); // apiKey might not exist
229
+ * const cleaned = omitCredentials(partialUser); // { id: 1, name: "Alice" }
230
+ * ```
231
+ */
232
+ export const omit: OmitFnOverload = (<
233
+ const R extends UnknownRecord,
234
+ const Keys extends readonly (keyof R)[],
235
+ >(
236
+ ...args:
237
+ | readonly [record: R, keys: Keys]
238
+ | readonly [keys: readonly PropertyKey[]]
239
+ ):
240
+ | Omit<R, ArrayElement<Keys>>
241
+ | ((record: R) => Omit<R, ArrayElement<Keys>>) => {
242
+ switch (args.length) {
243
+ case 2: {
244
+ const [record, keys] = args;
245
+ const keysSet = new Set<keyof R>(keys);
246
+
247
+ return Object.fromEntries(
248
+ Object.entries(record).filter(([k, _v]) => !keysSet.has(k)),
249
+ ) as never;
250
+ }
251
+
252
+ case 1: {
253
+ const [keys] = args;
254
+ return <R2 extends UnknownRecord>(record: R2) =>
255
+ omit(record, keys as readonly (keyof R2)[]) as Omit<
256
+ R2,
257
+ ArrayElement<Keys>
258
+ >;
259
+ }
260
+ }
261
+ }) as OmitFnOverload;
262
+
263
+ type OmitFnOverload = {
264
+ <const R extends UnknownRecord, const Keys extends readonly (keyof R)[]>(
265
+ record: R,
266
+ keys: Keys,
267
+ ): Omit<R, ArrayElement<Keys>>;
268
+
269
+ // Curried version
270
+ <const Keys extends readonly PropertyKey[]>(
271
+ keys: Keys,
272
+ ): <const R extends UnknownRecord>(
273
+ record: R,
274
+ ) => Omit<R, ArrayElement<Keys>>;
275
+ };
276
+
277
+ /**
278
+ * Creates an object from an array of key-value pairs with precise TypeScript typing.
279
+ * This is a type-safe wrapper around `Object.fromEntries` that provides better type inference
280
+ * and compile-time guarantees about the resulting object structure.
281
+ *
282
+ * **Type Behavior**:
283
+ * - When entries is a fixed-length tuple, the exact object type is inferred
284
+ * - When entries has dynamic length with union key types, `Partial` is applied to prevent
285
+ * incorrect assumptions about which keys will be present
286
+ *
287
+ * @template Entries - The readonly array type of key-value entry tuples
288
+ * @param entries - An array of readonly key-value entry tuples `[key, value]`
289
+ * @returns An object created from the entries with precise typing
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * // Fixed entries with precise typing
294
+ * const fixedEntries = [
295
+ * ['name', 'Alice'],
296
+ * ['age', 30],
297
+ * ['active', true]
298
+ * ] as const;
299
+ *
300
+ * const user = Obj.fromEntries(fixedEntries);
301
+ * // Type: { readonly name: "Alice"; readonly age: 30; readonly active: true }
302
+ * // Value: { name: "Alice", age: 30, active: true }
303
+ *
304
+ * // Simple coordinate example
305
+ * const coordEntries = [['x', 1], ['y', 3]] as const;
306
+ * const point = Obj.fromEntries(coordEntries);
307
+ * // Type: { readonly x: 1; readonly y: 3 }
308
+ * // Value: { x: 1, y: 3 }
309
+ *
310
+ * // Dynamic entries with union keys
311
+ * const dynamicEntries: Array<['name' | 'email', string]> = [
312
+ * ['name', 'Alice']
313
+ * // email might or might not be present
314
+ * ];
315
+ * const partialUser = Obj.fromEntries(dynamicEntries);
316
+ * // Type: Partial<{ name: string; email: string }>
317
+ * // This prevents assuming both 'name' and 'email' are always present
318
+ *
319
+ * // Creating configuration objects
320
+ * const configEntries = [
321
+ * ['apiUrl', 'https://api.example.com'],
322
+ * ['timeout', 5000],
323
+ * ['retries', 3],
324
+ * ['debug', false]
325
+ * ] as const;
326
+ * const config = Obj.fromEntries(configEntries);
327
+ * // Precise types for each configuration value
328
+ *
329
+ * // Converting Maps to objects
330
+ * const settingsMap = new Map([
331
+ * ['theme', 'dark'],
332
+ * ['language', 'en'],
333
+ * ['notifications', true]
334
+ * ] as const);
335
+ * const settings = Obj.fromEntries([...settingsMap]);
336
+ *
337
+ * // Building objects from computed entries
338
+ * const keys = ['a', 'b', 'c'] as const;
339
+ * const values = [1, 2, 3] as const;
340
+ * const computedEntries = keys.map((k, i) => [k, values[i]] as const);
341
+ * const computed = Obj.fromEntries(computedEntries);
342
+ * // Type reflects the specific key-value associations
343
+ *
344
+ * // Error handling with validation
345
+ * function createUserFromEntries(entries: ReadonlyArray<readonly [string, unknown]>) {
346
+ * const user = Obj.fromEntries(entries);
347
+ * // Type is Partial<Record<string, unknown>> - safe for dynamic data
348
+ *
349
+ * if ('name' in user && typeof user.name === 'string') {
350
+ * // Type narrowing works correctly
351
+ * return { name: user.name, ...user };
352
+ * }
353
+ * throw new Error('Invalid user data');
354
+ * }
355
+ * ```
356
+ */
357
+ export const fromEntries = <
358
+ const Entries extends readonly (readonly [PropertyKey, unknown])[],
359
+ >(
360
+ entries: Entries,
361
+ ): IsFixedLengthList<Entries> extends true
362
+ ? TsVerifiedInternals.EntriesToObject<Entries>
363
+ : TsVerifiedInternals.PartialIfKeyIsUnion<
364
+ TsVerifiedInternals.KeysOfEntries<Entries>,
365
+ Record<
366
+ TsVerifiedInternals.KeysOfEntries<Entries>,
367
+ TsVerifiedInternals.ValuesOfEntries<Entries>
368
+ >
369
+ > => Object.fromEntries(entries) as never;
370
+
371
+ /**
372
+ * @internal
373
+ * Internal type utilities for the Obj module.
374
+ */
375
+ declare namespace TsVerifiedInternals {
376
+ type RecursionLimit = 20;
377
+
378
+ /**
379
+ * - `[['x', 1], ['y', 3]]` -> `{ x: 1, y: 3 }`
380
+ */
381
+ export type EntriesToObject<
382
+ Entries extends readonly (readonly [PropertyKey, unknown])[],
383
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
384
+ > = Readonly<EntriesToObjectImpl<{}, Entries>>;
385
+
386
+ /** @internal */
387
+ type EntriesToObjectImpl<
388
+ R,
389
+ Entries extends readonly (readonly [PropertyKey, unknown])[],
390
+ > =
391
+ TypeEq<Entries['length'], 0> extends true
392
+ ? R
393
+ : EntriesToObjectImpl<
394
+ R & Readonly<Record<Entries[0][0], Entries[0][1]>>,
395
+ List.Tail<Entries>
396
+ >;
397
+
398
+ /**
399
+ * - `['x' | 'y' | 'z', number][]]` -> `'x' | 'y' | 'z'`
400
+ * - `[['a' | 'b' | 'c', number], ...['x' | 'y' | 'z', number][]]` -> `'a' | 'b' |
401
+ * 'c' | 'x' | 'y' | 'z'`
402
+ *
403
+ * @note To handle the second example above, recursion needs to be performed on infinite-length Entries,
404
+ * but since the timing to stop cannot be determined, a recursion limit is set.
405
+ */
406
+ export type KeysOfEntries<
407
+ Entries extends readonly (readonly [PropertyKey, unknown])[],
408
+ > = KeysOfEntriesImpl<never, Entries, RecursionLimit>;
409
+
410
+ type KeysOfEntriesImpl<
411
+ K,
412
+ Entries extends readonly (readonly [PropertyKey, unknown])[],
413
+ RemainingNumRecursions extends number,
414
+ > =
415
+ TypeEq<RemainingNumRecursions, 0> extends true
416
+ ? K
417
+ : TypeEq<Entries['length'], 0> extends true
418
+ ? K
419
+ : KeysOfEntriesImpl<
420
+ K | Entries[0][0],
421
+ List.Tail<Entries>,
422
+ Decrement<RemainingNumRecursions>
423
+ >;
424
+
425
+ export type ValuesOfEntries<
426
+ Entries extends readonly (readonly [PropertyKey, unknown])[],
427
+ > = ValuesOfEntriesImpl<never, Entries, RecursionLimit>;
428
+
429
+ type ValuesOfEntriesImpl<
430
+ K,
431
+ Entries extends readonly (readonly [PropertyKey, unknown])[],
432
+ RemainingNumRecursions extends number,
433
+ > =
434
+ TypeEq<RemainingNumRecursions, 0> extends true
435
+ ? K
436
+ : TypeEq<Entries['length'], 0> extends true
437
+ ? K
438
+ : ValuesOfEntriesImpl<
439
+ K | Entries[0][1],
440
+ List.Tail<Entries>,
441
+ Decrement<RemainingNumRecursions>
442
+ >;
443
+
444
+ export type PartialIfKeyIsUnion<K, T> =
445
+ IsUnion<K> extends true ? Partial<T> : T;
446
+ }
447
+ }
@@ -0,0 +1,124 @@
1
+ import { pipe } from '../functional/index.mjs';
2
+ import { Obj } from './object.mjs';
3
+
4
+ describe('shallowEq', () => {
5
+ test('truthy case 1', () => {
6
+ expect(Obj.shallowEq({ x: 0 }, { x: 0 })).toBe(true);
7
+ });
8
+
9
+ test('truthy case 2', () => {
10
+ expect(Obj.shallowEq({}, {})).toBe(true);
11
+ });
12
+
13
+ test('falsy case 1', () => {
14
+ expect(Obj.shallowEq({ x: 0 }, { x: 0, y: 0 })).toBe(false);
15
+ });
16
+
17
+ test('falsy case 2', () => {
18
+ expect(Obj.shallowEq({ x: 0, y: 0 }, { x: 0 })).toBe(false);
19
+ });
20
+
21
+ test('falsy case 3', () => {
22
+ expect(Obj.shallowEq({ x: 0 }, { y: 0 })).toBe(false);
23
+ });
24
+
25
+ test('falsy case 4', () => {
26
+ expect(Obj.shallowEq({ x: [] }, { y: 0 })).toBe(false);
27
+ });
28
+ });
29
+
30
+ describe('pick', () => {
31
+ test('truthy case 1', () => {
32
+ expect(Obj.pick({ a: 1, b: 2, c: 3 }, ['a', 'b'])).toStrictEqual({
33
+ a: 1,
34
+ b: 2,
35
+ });
36
+ });
37
+
38
+ test('should support curried form', () => {
39
+ const pickAB = Obj.pick(['a', 'b']);
40
+ const result = pickAB({ a: 1, b: 2, c: 3, d: 4 });
41
+ expect(result).toStrictEqual({ a: 1, b: 2 });
42
+ });
43
+
44
+ test('should work with pipe when curried', () => {
45
+ const pickIdAndName = Obj.pick(['id', 'name']);
46
+ const user = { id: 1, name: 'Alice', email: 'alice@example.com', age: 30 };
47
+
48
+ const result = pipe(user).map(pickIdAndName).value;
49
+ expect(result).toStrictEqual({ id: 1, name: 'Alice' });
50
+ });
51
+
52
+ test('should handle empty keys in curried form', () => {
53
+ const pickNone = Obj.pick([]);
54
+ const result = pickNone({ a: 1, b: 2 });
55
+ expect(result).toStrictEqual({});
56
+ });
57
+
58
+ test('should work for records that only partially contain the key in curried form', () => {
59
+ const pickVisible = Obj.pick(['name', 'age']);
60
+ const user = {
61
+ id: 1,
62
+ name: 'Alice',
63
+ password: 'secret123',
64
+ } as const;
65
+
66
+ const result = pipe(user).map(pickVisible).value satisfies {
67
+ name: string;
68
+ };
69
+ expect(result).toStrictEqual({ name: 'Alice' });
70
+ });
71
+ });
72
+
73
+ describe('omit', () => {
74
+ test('truthy case 1', () => {
75
+ expect(Obj.omit({ a: 1, b: 2, c: 3 }, ['c'])).toStrictEqual({ a: 1, b: 2 });
76
+ });
77
+
78
+ test('should support curried form', () => {
79
+ const omitC = Obj.omit(['c']);
80
+ const result = omitC({ a: 1, b: 2, c: 3, d: 4 });
81
+ expect(result).toStrictEqual({ a: 1, b: 2, d: 4 });
82
+ });
83
+
84
+ test('should work with pipe when curried', () => {
85
+ const omitSensitive = Obj.omit(['password', 'email']);
86
+ const user = {
87
+ id: 1,
88
+ name: 'Alice',
89
+ email: 'alice@example.com',
90
+ password: 'secret123',
91
+ };
92
+
93
+ const result = pipe(user).map(omitSensitive).value;
94
+ expect(result).toStrictEqual({ id: 1, name: 'Alice' });
95
+ });
96
+
97
+ test('should handle empty keys in curried form', () => {
98
+ const omitNone = Obj.omit([]);
99
+ const original = { a: 1, b: 2, c: 3 };
100
+ const result = omitNone(original);
101
+ expect(result).toStrictEqual(original);
102
+ });
103
+
104
+ test('should omit multiple keys in curried form', () => {
105
+ const omitBAndD = Obj.omit(['b', 'd']);
106
+ const result = omitBAndD({ a: 1, b: 2, c: 3, d: 4, e: 5 });
107
+ expect(result).toStrictEqual({ a: 1, c: 3, e: 5 });
108
+ });
109
+
110
+ test('should work for records that only partially contain the key in curried form', () => {
111
+ const omitSensitive = Obj.omit(['password', 'email']);
112
+ const user = {
113
+ id: 1,
114
+ name: 'Alice',
115
+ password: 'secret123',
116
+ } as const;
117
+
118
+ const result = pipe(user).map(omitSensitive).value satisfies {
119
+ id: number;
120
+ name: string;
121
+ };
122
+ expect(result).toStrictEqual({ id: 1, name: 'Alice' });
123
+ });
124
+ });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Casts a readonly type `T` to its `Mutable<T>` equivalent.
3
+ *
4
+ * **⚠️ Safety Warning**: This is a type assertion that bypasses TypeScript's immutability guarantees.
5
+ * The runtime value remains unchanged - only the type system's view of it changes.
6
+ * Use with caution as it can lead to unexpected mutations of data that was intended to be immutable.
7
+ *
8
+ * @template T - The type of the readonly value
9
+ * @param readonlyValue - The readonly value to cast to mutable
10
+ * @returns The same value with readonly modifiers removed from its type
11
+ *
12
+ * @example Basic usage with arrays and objects
13
+ * ```typescript
14
+ * const readonlyArr: readonly number[] = [1, 2, 3];
15
+ * const mutableArr = castMutable(readonlyArr);
16
+ * mutableArr.push(4); // Now allowed by TypeScript
17
+ *
18
+ * const readonlyObj: { readonly x: number } = { x: 1 };
19
+ * const mutableObj = castMutable(readonlyObj);
20
+ * mutableObj.x = 2; // Now allowed by TypeScript
21
+ * ```
22
+ *
23
+ * @example When to use - Working with third-party APIs
24
+ * ```typescript
25
+ * // Some APIs require mutable arrays but you have readonly data
26
+ * const readonlyData: readonly string[] = ['a', 'b', 'c'];
27
+ * const sortedData = castMutable([...readonlyData]); // Create a copy first!
28
+ * legacyApi.sort(sortedData); // API mutates the array
29
+ * ```
30
+ *
31
+ * @example Anti-pattern - Avoid mutating shared data
32
+ * ```typescript
33
+ * // ❌ Bad: Mutating data that other code expects to be immutable
34
+ * const sharedConfig: Readonly<Config> = getConfig();
35
+ * const mutable = castMutable(sharedConfig);
36
+ * mutable.apiKey = 'new-key'; // Dangerous! Other code expects this to be immutable
37
+ *
38
+ * // ✅ Good: Create a copy if you need to mutate
39
+ * const configCopy = castMutable({ ...sharedConfig });
40
+ * configCopy.apiKey = 'new-key'; // Safe - operating on a copy
41
+ * ```
42
+ *
43
+ * @see castDeepMutable - For deeply nested readonly structures
44
+ * @see castReadonly - For the opposite operation
45
+ */
46
+ export const castMutable = <T,>(readonlyValue: T): Mutable<T> =>
47
+ readonlyValue as Mutable<T>;
48
+
49
+ /**
50
+ * Casts a readonly type `T` to its `DeepMutable<T>` equivalent, recursively removing all readonly modifiers.
51
+ *
52
+ * **⚠️ Safety Warning**: This recursively bypasses ALL immutability guarantees in nested structures.
53
+ * Extremely dangerous for complex data structures as it allows mutation at any depth.
54
+ * The runtime value is unchanged - only TypeScript's type checking is affected.
55
+ *
56
+ * @template T - The type of the deeply readonly value
57
+ * @param readonlyValue - The deeply readonly value to cast to deeply mutable
58
+ * @returns The same value with all readonly modifiers recursively removed from its type
59
+ *
60
+ * @example Basic usage with nested structures
61
+ * ```typescript
62
+ * const readonlyNested: {
63
+ * readonly a: { readonly b: readonly number[] }
64
+ * } = { a: { b: [1, 2, 3] } };
65
+ *
66
+ * const mutableNested = castDeepMutable(readonlyNested);
67
+ * mutableNested.a.b.push(4); // Mutations allowed at all levels
68
+ * mutableNested.a = { b: [5, 6] }; // Can replace entire objects
69
+ * mutableNested.a.b[0] = 99; // Can mutate array elements
70
+ * ```
71
+ *
72
+ * @example Practical use case - Working with immutable state updates
73
+ * ```typescript
74
+ * // When you need to perform multiple mutations before creating new immutable state
75
+ * const currentState: DeepReadonly<AppState> = getState();
76
+ * const draft = castDeepMutable(structuredClone(currentState)); // Clone first!
77
+ *
78
+ * // Perform multiple mutations on the draft
79
+ * draft.users.push(newUser);
80
+ * draft.settings.theme = 'dark';
81
+ * draft.data.items[0].completed = true;
82
+ *
83
+ * // Create new immutable state from the mutated draft
84
+ * const newState = castDeepReadonly(draft);
85
+ * setState(newState);
86
+ * ```
87
+ *
88
+ * @example Type complexity with generics
89
+ * ```typescript
90
+ * type DeepReadonlyUser = DeepReadonly<{
91
+ * id: number;
92
+ * profile: {
93
+ * settings: {
94
+ * preferences: string[];
95
+ * };
96
+ * };
97
+ * }>;
98
+ *
99
+ * function updateUserPreferences(user: DeepReadonlyUser, newPref: string) {
100
+ * // Create a mutable copy to work with
101
+ * const mutableUser = castDeepMutable(structuredClone(user));
102
+ * mutableUser.profile.settings.preferences.push(newPref);
103
+ * return castDeepReadonly(mutableUser);
104
+ * }
105
+ * ```
106
+ *
107
+ * @see castMutable - For shallow mutability casting
108
+ * @see castDeepReadonly - For the opposite operation
109
+ * @see structuredClone - Recommended for creating safe copies before mutation
110
+ */
111
+ export const castDeepMutable = <T,>(readonlyValue: T): DeepMutable<T> =>
112
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
113
+ readonlyValue as DeepMutable<T>;