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.
- package/LICENSE +201 -0
- package/README.md +534 -0
- package/package.json +101 -0
- package/src/array/array-utils-creation.test.mts +443 -0
- package/src/array/array-utils-modification.test.mts +197 -0
- package/src/array/array-utils-overload-type-error.test.mts +149 -0
- package/src/array/array-utils-reducing-value.test.mts +425 -0
- package/src/array/array-utils-search.test.mts +169 -0
- package/src/array/array-utils-set-op.test.mts +335 -0
- package/src/array/array-utils-slice-clamped.test.mts +113 -0
- package/src/array/array-utils-slicing.test.mts +316 -0
- package/src/array/array-utils-transformation.test.mts +790 -0
- package/src/array/array-utils-validation.test.mts +492 -0
- package/src/array/array-utils.mts +4000 -0
- package/src/array/array.test.mts +146 -0
- package/src/array/index.mts +2 -0
- package/src/array/tuple-utils.mts +519 -0
- package/src/array/tuple-utils.test.mts +518 -0
- package/src/collections/imap-mapped.mts +801 -0
- package/src/collections/imap-mapped.test.mts +860 -0
- package/src/collections/imap.mts +651 -0
- package/src/collections/imap.test.mts +932 -0
- package/src/collections/index.mts +6 -0
- package/src/collections/iset-mapped.mts +889 -0
- package/src/collections/iset-mapped.test.mts +1187 -0
- package/src/collections/iset.mts +682 -0
- package/src/collections/iset.test.mts +1084 -0
- package/src/collections/queue.mts +390 -0
- package/src/collections/queue.test.mts +282 -0
- package/src/collections/stack.mts +423 -0
- package/src/collections/stack.test.mts +225 -0
- package/src/expect-type.mts +206 -0
- package/src/functional/index.mts +4 -0
- package/src/functional/match.mts +300 -0
- package/src/functional/match.test.mts +177 -0
- package/src/functional/optional.mts +733 -0
- package/src/functional/optional.test.mts +619 -0
- package/src/functional/pipe.mts +212 -0
- package/src/functional/pipe.test.mts +85 -0
- package/src/functional/result.mts +1134 -0
- package/src/functional/result.test.mts +777 -0
- package/src/globals.d.mts +38 -0
- package/src/guard/has-key.mts +119 -0
- package/src/guard/has-key.test.mts +219 -0
- package/src/guard/index.mts +7 -0
- package/src/guard/is-non-empty-string.mts +108 -0
- package/src/guard/is-non-empty-string.test.mts +91 -0
- package/src/guard/is-non-null-object.mts +106 -0
- package/src/guard/is-non-null-object.test.mts +90 -0
- package/src/guard/is-primitive.mts +165 -0
- package/src/guard/is-primitive.test.mts +102 -0
- package/src/guard/is-record.mts +153 -0
- package/src/guard/is-record.test.mts +112 -0
- package/src/guard/is-type.mts +450 -0
- package/src/guard/is-type.test.mts +496 -0
- package/src/guard/key-is-in.mts +163 -0
- package/src/guard/key-is-in.test.mts +19 -0
- package/src/index.mts +10 -0
- package/src/iterator/index.mts +1 -0
- package/src/iterator/range.mts +120 -0
- package/src/iterator/range.test.mts +33 -0
- package/src/json/index.mts +1 -0
- package/src/json/json.mts +711 -0
- package/src/json/json.test.mts +628 -0
- package/src/number/branded-types/finite-number.mts +354 -0
- package/src/number/branded-types/finite-number.test.mts +135 -0
- package/src/number/branded-types/index.mts +26 -0
- package/src/number/branded-types/int.mts +278 -0
- package/src/number/branded-types/int.test.mts +140 -0
- package/src/number/branded-types/int16.mts +192 -0
- package/src/number/branded-types/int16.test.mts +170 -0
- package/src/number/branded-types/int32.mts +193 -0
- package/src/number/branded-types/int32.test.mts +170 -0
- package/src/number/branded-types/non-negative-finite-number.mts +223 -0
- package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
- package/src/number/branded-types/non-negative-int16.mts +187 -0
- package/src/number/branded-types/non-negative-int16.test.mts +201 -0
- package/src/number/branded-types/non-negative-int32.mts +187 -0
- package/src/number/branded-types/non-negative-int32.test.mts +204 -0
- package/src/number/branded-types/non-zero-finite-number.mts +229 -0
- package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
- package/src/number/branded-types/non-zero-int.mts +167 -0
- package/src/number/branded-types/non-zero-int.test.mts +177 -0
- package/src/number/branded-types/non-zero-int16.mts +196 -0
- package/src/number/branded-types/non-zero-int16.test.mts +195 -0
- package/src/number/branded-types/non-zero-int32.mts +196 -0
- package/src/number/branded-types/non-zero-int32.test.mts +197 -0
- package/src/number/branded-types/non-zero-safe-int.mts +196 -0
- package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
- package/src/number/branded-types/non-zero-uint16.mts +189 -0
- package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
- package/src/number/branded-types/non-zero-uint32.mts +189 -0
- package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
- package/src/number/branded-types/positive-finite-number.mts +241 -0
- package/src/number/branded-types/positive-finite-number.test.mts +204 -0
- package/src/number/branded-types/positive-int.mts +304 -0
- package/src/number/branded-types/positive-int.test.mts +176 -0
- package/src/number/branded-types/positive-int16.mts +188 -0
- package/src/number/branded-types/positive-int16.test.mts +197 -0
- package/src/number/branded-types/positive-int32.mts +188 -0
- package/src/number/branded-types/positive-int32.test.mts +197 -0
- package/src/number/branded-types/positive-safe-int.mts +187 -0
- package/src/number/branded-types/positive-safe-int.test.mts +210 -0
- package/src/number/branded-types/positive-uint16.mts +188 -0
- package/src/number/branded-types/positive-uint16.test.mts +203 -0
- package/src/number/branded-types/positive-uint32.mts +188 -0
- package/src/number/branded-types/positive-uint32.test.mts +203 -0
- package/src/number/branded-types/safe-int.mts +291 -0
- package/src/number/branded-types/safe-int.test.mts +170 -0
- package/src/number/branded-types/safe-uint.mts +187 -0
- package/src/number/branded-types/safe-uint.test.mts +176 -0
- package/src/number/branded-types/uint.mts +179 -0
- package/src/number/branded-types/uint.test.mts +158 -0
- package/src/number/branded-types/uint16.mts +186 -0
- package/src/number/branded-types/uint16.test.mts +170 -0
- package/src/number/branded-types/uint32.mts +218 -0
- package/src/number/branded-types/uint32.test.mts +170 -0
- package/src/number/enum/index.mts +2 -0
- package/src/number/enum/int8.mts +344 -0
- package/src/number/enum/int8.test.mts +180 -0
- package/src/number/enum/uint8.mts +293 -0
- package/src/number/enum/uint8.test.mts +164 -0
- package/src/number/index.mts +4 -0
- package/src/number/num.mts +604 -0
- package/src/number/num.test.mts +242 -0
- package/src/number/refined-number-utils.mts +566 -0
- package/src/object/index.mts +1 -0
- package/src/object/object.mts +447 -0
- package/src/object/object.test.mts +124 -0
- package/src/others/cast-mutable.mts +113 -0
- package/src/others/cast-readonly.mts +192 -0
- package/src/others/cast-readonly.test.mts +89 -0
- package/src/others/if-then.mts +98 -0
- package/src/others/if-then.test.mts +75 -0
- package/src/others/index.mts +7 -0
- package/src/others/map-nullable.mts +172 -0
- package/src/others/map-nullable.test.mts +297 -0
- package/src/others/memoize-function.mts +196 -0
- package/src/others/memoize-function.test.mts +168 -0
- package/src/others/tuple.mts +160 -0
- package/src/others/tuple.test.mts +11 -0
- package/src/others/unknown-to-string.mts +215 -0
- 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>;
|