ts-data-forge 14.10.0 → 14.10.2

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 (103) hide show
  1. package/README.md +5 -5
  2. package/dist/array/impl/array-utils-element-access.d.mts.map +1 -1
  3. package/dist/array/impl/array-utils-element-access.mjs +4 -4
  4. package/dist/array/impl/array-utils-element-access.mjs.map +1 -1
  5. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts +8 -8
  6. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts.map +1 -1
  7. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs +28 -28
  8. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs.map +1 -1
  9. package/dist/array/impl/array-utils-length-bounded-array-guard.d.mts.map +1 -1
  10. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs +4 -4
  11. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs.map +1 -1
  12. package/dist/array/impl/array-utils-length-bounded-tuple-cast.d.mts +3 -4
  13. package/dist/array/impl/array-utils-length-bounded-tuple-cast.d.mts.map +1 -1
  14. package/dist/array/impl/array-utils-length-bounded-tuple-cast.mjs +28 -29
  15. package/dist/array/impl/array-utils-length-bounded-tuple-cast.mjs.map +1 -1
  16. package/dist/array/impl/array-utils-length-bounded-tuple-guard.d.mts.map +1 -1
  17. package/dist/array/impl/array-utils-length-bounded-tuple-guard.mjs +3 -3
  18. package/dist/array/impl/array-utils-length-bounded-tuple-guard.mjs.map +1 -1
  19. package/dist/array/impl/array-utils-modification.d.mts +1 -1
  20. package/dist/array/impl/array-utils-modification.d.mts.map +1 -1
  21. package/dist/array/impl/array-utils-modification.mjs +15 -15
  22. package/dist/array/impl/array-utils-modification.mjs.map +1 -1
  23. package/dist/array/impl/array-utils-reducing-value.d.mts.map +1 -1
  24. package/dist/array/impl/array-utils-reducing-value.mjs +12 -12
  25. package/dist/array/impl/array-utils-reducing-value.mjs.map +1 -1
  26. package/dist/array/impl/array-utils-search.d.mts.map +1 -1
  27. package/dist/array/impl/array-utils-search.mjs +56 -56
  28. package/dist/array/impl/array-utils-search.mjs.map +1 -1
  29. package/dist/array/impl/array-utils-slicing.d.mts.map +1 -1
  30. package/dist/array/impl/array-utils-slicing.mjs +3 -3
  31. package/dist/array/impl/array-utils-slicing.mjs.map +1 -1
  32. package/dist/array/impl/array-utils-transformation.d.mts +25 -25
  33. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  34. package/dist/array/impl/array-utils-transformation.mjs +22 -22
  35. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  36. package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
  37. package/dist/array/impl/array-utils-validation.mjs +2 -2
  38. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  39. package/dist/collections/imap-mapped.d.mts +2 -2
  40. package/dist/collections/imap-mapped.mjs +6 -3
  41. package/dist/collections/imap-mapped.mjs.map +1 -1
  42. package/dist/collections/imap.d.mts +3 -4
  43. package/dist/collections/imap.d.mts.map +1 -1
  44. package/dist/collections/imap.mjs +6 -4
  45. package/dist/collections/imap.mjs.map +1 -1
  46. package/dist/collections/iset-mapped.d.mts +1 -2
  47. package/dist/collections/iset-mapped.d.mts.map +1 -1
  48. package/dist/collections/iset-mapped.mjs.map +1 -1
  49. package/dist/collections/iset.d.mts +4 -5
  50. package/dist/collections/iset.d.mts.map +1 -1
  51. package/dist/collections/iset.mjs +0 -1
  52. package/dist/collections/iset.mjs.map +1 -1
  53. package/dist/json/json.d.mts.map +1 -1
  54. package/dist/json/json.mjs +13 -47
  55. package/dist/json/json.mjs.map +1 -1
  56. package/dist/number/num.d.mts +12 -8
  57. package/dist/number/num.d.mts.map +1 -1
  58. package/dist/number/num.mjs +5 -1
  59. package/dist/number/num.mjs.map +1 -1
  60. package/dist/object/object.d.mts +10 -2
  61. package/dist/object/object.d.mts.map +1 -1
  62. package/dist/object/object.mjs +16 -4
  63. package/dist/object/object.mjs.map +1 -1
  64. package/dist/others/fast-deep-equal.d.mts.map +1 -1
  65. package/dist/others/fast-deep-equal.mjs +10 -7
  66. package/dist/others/fast-deep-equal.mjs.map +1 -1
  67. package/dist/others/replace-equal-deep.d.mts +9 -9
  68. package/dist/others/replace-equal-deep.mjs +22 -15
  69. package/dist/others/replace-equal-deep.mjs.map +1 -1
  70. package/dist/string/str.d.mts +2 -2
  71. package/package.json +12 -12
  72. package/src/array/impl/array-utils-element-access.mts +11 -11
  73. package/src/array/impl/array-utils-length-bounded-array-cast.mts +69 -69
  74. package/src/array/impl/array-utils-length-bounded-array-guard.mts +21 -21
  75. package/src/array/impl/array-utils-length-bounded-array-guard.test.mts +2 -2
  76. package/src/array/impl/array-utils-length-bounded-tuple-cast.mts +64 -65
  77. package/src/array/impl/array-utils-length-bounded-tuple-guard.mts +15 -15
  78. package/src/array/impl/array-utils-modification.mts +54 -54
  79. package/src/array/impl/array-utils-reducing-value.mts +55 -55
  80. package/src/array/impl/array-utils-search.mts +114 -114
  81. package/src/array/impl/array-utils-slicing.mts +16 -16
  82. package/src/array/impl/array-utils-transformation.mts +96 -96
  83. package/src/array/impl/array-utils-transformation.test.mts +54 -5
  84. package/src/array/impl/array-utils-validation.mts +19 -19
  85. package/src/array/impl/array-utils-validation.test.mts +1 -1
  86. package/src/collections/imap-mapped.mts +8 -6
  87. package/src/collections/imap-mapped.test.mts +27 -0
  88. package/src/collections/imap.mts +9 -8
  89. package/src/collections/imap.test.mts +14 -1
  90. package/src/collections/iset-mapped.mts +1 -2
  91. package/src/collections/iset.mts +4 -5
  92. package/src/collections/iset.test.mts +1 -1
  93. package/src/json/json.mts +17 -57
  94. package/src/json/json.test.mts +32 -0
  95. package/src/number/num.mts +21 -17
  96. package/src/number/num.test.mts +13 -1
  97. package/src/object/object.mts +34 -6
  98. package/src/object/object.test.mts +55 -0
  99. package/src/others/fast-deep-equal.mts +17 -10
  100. package/src/others/fast-deep-equal.test.mts +141 -29
  101. package/src/others/replace-equal-deep.mts +48 -41
  102. package/src/others/replace-equal-deep.test.mts +39 -7
  103. package/src/string/str.mts +2 -2
package/src/json/json.mts CHANGED
@@ -3,8 +3,7 @@ import type {
3
3
  UintRangeInclusive,
4
4
  UnknownRecord,
5
5
  } from 'ts-type-forge';
6
- import { Arr } from '../array/index.mjs';
7
- import { pipe, Result } from '../functional/index.mjs';
6
+ import { Result } from '../functional/index.mjs';
8
7
  import { isRecord } from '../guard/index.mjs';
9
8
  import { castMutable, unknownToString } from '../others/index.mjs';
10
9
 
@@ -323,63 +322,24 @@ export namespace Json {
323
322
  value: UnknownRecord,
324
323
  space?: UintRangeInclusive<1, 10> | string,
325
324
  ): Result<string | undefined, string> => {
326
- const allKeys = pipe(keysDeep(value))
327
- .map(Arr.uniq)
328
- .map((ks) => ks.toSorted()).value;
325
+ // The keys are collected by a first pass with a function replacer, which
326
+ // `JSON.stringify` calls at every nesting level with the value after
327
+ // `toJSON`, so the allowlist of the second pass misses none of them. The
328
+ // allowlist also fixes the order, sorting integer-like keys as strings.
329
+ const mut_keys = new Set<string>();
329
330
 
330
- return stringifySelected(value, allKeys, space);
331
- };
332
- }
333
-
334
- /**
335
- * @param obj - The record to extract keys from. Must be a plain object.
336
- * @param mut_keys - A mutable array to accumulate the collected keys. This
337
- * array will be modified in-place by the function for performance reasons.
338
- * @internal
339
- * Recursively collects all property keys from a nested object structure.
340
- *
341
- * This helper function traverses an object and its nested objects and arrays,
342
- * collecting all string keys found at any level of nesting. The function mutates
343
- * the provided keys array for performance reasons.
344
- */
345
- const keysDeepImpl = (
346
- obj: UnknownRecord,
347
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
348
- mut_keys: string[],
349
- ): void => {
350
- for (const [k, v] of Object.entries(obj)) {
351
- mut_keys.push(k);
352
-
353
- if (isRecord(v)) {
354
- keysDeepImpl(v, mut_keys);
355
- }
356
-
357
- if (Array.isArray(v)) {
358
- for (const li of v) {
359
- if (isRecord(li)) {
360
- keysDeepImpl(li, mut_keys);
331
+ const collected = stringify(value, (_key, v) => {
332
+ if (isRecord(v)) {
333
+ for (const k of Object.keys(v)) {
334
+ mut_keys.add(k);
361
335
  }
362
336
  }
363
- }
364
- }
365
- };
366
337
 
367
- /**
368
- * @param obj - The record to extract keys from. Must be a plain object.
369
- * @returns A readonly array of all string keys found in the object and its
370
- * nested objects/arrays. May contain duplicates if the same key appears at
371
- * multiple levels.
372
- * @internal
373
- * Extracts all property keys from a nested object structure into a flat array.
374
- *
375
- * This function serves as a safe wrapper around `keysDeepImpl`, creating a new
376
- * mutable array and passing it to the recursive implementation. The result
377
- * contains all keys found at any level of nesting within the input object.
378
- */
379
- const keysDeep = (obj: UnknownRecord): readonly string[] => {
380
- const mut_keys: string[] = [];
338
+ return v;
339
+ });
381
340
 
382
- keysDeepImpl(obj, mut_keys);
383
-
384
- return mut_keys;
385
- };
341
+ return Result.isErr(collected)
342
+ ? collected
343
+ : stringifySelected(value, Array.from(mut_keys).toSorted(), space);
344
+ };
345
+ }
@@ -709,6 +709,38 @@ describe('stringifySortedKey', () => {
709
709
  }
710
710
  });
711
711
 
712
+ test('keeps the properties of an object nested in an array of arrays', () => {
713
+ const result = Json.stringifySortedKey({ a: [[{ c: 1, b: 2 }]] });
714
+
715
+ assert.isTrue(Result.isOk(result));
716
+
717
+ expect(result.value).toBe('{"a":[[{"b":2,"c":1}]]}');
718
+
719
+ const other = Json.stringifySortedKey({ a: [[{ c: 1, b: 3 }]] });
720
+
721
+ assert.isTrue(Result.isOk(other));
722
+
723
+ expect(other.value).not.toBe(result.value);
724
+ });
725
+
726
+ test('keeps the properties of what toJSON returns', () => {
727
+ const result = Json.stringifySortedKey({
728
+ x: { toJSON: () => ({ z: 1, y: 2 }) },
729
+ });
730
+
731
+ assert.isTrue(Result.isOk(result));
732
+
733
+ expect(result.value).toBe('{"x":{"y":2,"z":1}}');
734
+ });
735
+
736
+ test('sorts integer-like keys as strings', () => {
737
+ const result = Json.stringifySortedKey({ 2: 'b', 10: 'a', x: 'c' });
738
+
739
+ assert.isTrue(Result.isOk(result));
740
+
741
+ expect(result.value).toBe('{"10":"a","2":"b","x":"c"}');
742
+ });
743
+
712
744
  test('should handle empty object', () => {
713
745
  const result = Json.stringifySortedKey({});
714
746
 
@@ -120,8 +120,8 @@ export namespace Num {
120
120
  *
121
121
  * assert.strictEqual(Result.unwrapOk(Num.safeParseInt(' ')), undefined);
122
122
  *
123
- * // A value that overflows to Infinity is not an integer either, even
124
- * // though native `parseInt` reads a 1 out of it.
123
+ * // A value that overflows to Infinity is not an integer either, even though
124
+ * // native `parseInt` reads a 1 out of it.
125
125
  *
126
126
  * assert.strictEqual(Number.parseInt('1e400', 10), 1);
127
127
  *
@@ -135,10 +135,10 @@ export namespace Num {
135
135
  */
136
136
  export const safeParseInt = (s: string): Result<Int, Error> =>
137
137
  // The parsing itself is `SafeNumber.parseInteger` (Sumi D-49 (c)): one
138
- // implementation, in the package that owns the standard-library
139
- // boundary. What stays here is this function's own contract — the `Int`
140
- // brand on the success side, and an `Error` rather than a tagged record
141
- // on the failure side.
138
+ // implementation, in the package that owns the standard-library boundary.
139
+ // What stays here is this function's own contract — the `Int` brand on the
140
+ // success side, and an `Error` rather than a tagged record on the failure
141
+ // side.
142
142
  Result.match(SafeNumber.parseInteger(s), {
143
143
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
144
144
  ok: (n) => Result.ok(n as Int),
@@ -213,10 +213,10 @@ export namespace Num {
213
213
  */
214
214
  export const safeParseFloat = (s: string): Result<FiniteNumber, Error> =>
215
215
  // The parsing itself is `SafeNumber.parse` (Sumi D-49 (c)): one
216
- // implementation, in the package that owns the standard-library
217
- // boundary. What stays here is this function's own contract — the
218
- // `FiniteNumber` brand on the success side, and an `Error` rather than a
219
- // tagged record on the failure side.
216
+ // implementation, in the package that owns the standard-library boundary.
217
+ // What stays here is this function's own contract — the `FiniteNumber`
218
+ // brand on the success side, and an `Error` rather than a tagged record on
219
+ // the failure side.
220
220
  Result.match(SafeNumber.parse(s), {
221
221
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
222
222
  ok: (n) => Result.ok(n as FiniteNumber),
@@ -603,15 +603,19 @@ export namespace Num {
603
603
  /**
604
604
  * Performs integer division using floor division.
605
605
  *
606
- * Computes `⌊a / b⌋` by flooring both operands before division and then
607
- * flooring the result. This ensures integer arithmetic semantics.
606
+ * Computes `⌊⌊a⌋ / b⌋`: the dividend is floored, the divisor is used as is,
607
+ * and the quotient is floored. For a positive integer divisor this is
608
+ * `⌊a / b⌋`. The divisor is not floored, since a divisor in `(-1, 1)` would
609
+ * floor to `0` or `-1`.
608
610
  *
609
- * Note: Unlike `div`, this function does not enforce non-zero divisor at
610
- * compile time. Division by zero returns `NaN`.
611
+ * Like `div`, the divisor must be typed as `NonZeroNumber` or a non-zero
612
+ * numeric literal. A divisor of magnitude below `1` makes the quotient larger
613
+ * than the dividend, so it overflows to `±Infinity` once `|a / b|` exceeds
614
+ * `Number.MAX_VALUE` (e.g. `1e300 / 1e-10`), as `mul` can.
611
615
  *
612
616
  * @param a - The dividend
613
- * @param b - The divisor
614
- * @returns The integer quotient, or `NaN` if b is zero
617
+ * @param b - The divisor (must be non-zero, enforced by types)
618
+ * @returns The integer quotient `⌊⌊a⌋ / b⌋`
615
619
  */
616
620
  export const divInt = <
617
621
  A extends number,
@@ -621,7 +625,7 @@ export namespace Num {
621
625
  b: B,
622
626
  ): DivIntResult<A, B> => {
623
627
  // eslint-disable-next-line total-functions/no-partial-division
624
- const quotient = Math.floor(Math.floor(a) / Math.floor(b));
628
+ const quotient = Math.floor(Math.floor(a) / b);
625
629
 
626
630
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
627
631
  return quotient as DivIntResult<A, B>;
@@ -354,7 +354,19 @@ describe('Num test', () => {
354
354
 
355
355
  expect(Num.divInt(-7, 2)).toBe(-4); // floor division
356
356
 
357
- expect(Num.divInt(10.7, asNonZeroFiniteNumber(3.2))).toBe(3); // floors both operands
357
+ expect(Num.divInt(10.7, asNonZeroFiniteNumber(3.2))).toBe(3); // ⌊⌊10.7⌋ / 3.2⌋
358
+ });
359
+
360
+ test('a divisor in (-1, 1) is not floored to 0 or -1', () => {
361
+ expect(Num.divInt(5, asNonZeroFiniteNumber(0.5))).toBe(10);
362
+
363
+ expect(Num.divInt(0.3, asNonZeroFiniteNumber(0.5))).toBe(0);
364
+
365
+ expect(Num.divInt(-5, asNonZeroFiniteNumber(0.5))).toBe(-10);
366
+
367
+ expect(Num.divInt(5, asNonZeroFiniteNumber(-0.5))).toBe(-10);
368
+
369
+ expect(Num.divInt(7, asNonZeroFiniteNumber(-2.5))).toBe(-3);
358
370
  });
359
371
  });
360
372
 
@@ -91,6 +91,10 @@ export namespace Obj {
91
91
  * **Type Safety**: Only keys that exist in the source record type are
92
92
  * allowed, preventing runtime errors from accessing non-existent properties.
93
93
  *
94
+ * **Key handling**: a numeric key `1` matches the property `'1'`, and
95
+ * symbol-keyed properties are picked like string-keyed ones. Only own
96
+ * enumerable properties are read.
97
+ *
94
98
  * @example
95
99
  *
96
100
  * ```ts
@@ -150,12 +154,12 @@ export namespace Obj {
150
154
  case 2: {
151
155
  const [record, keys] = args;
152
156
 
153
- const keysSet = new Set<keyof R>(keys);
157
+ const keysSet = toOwnKeySet(keys);
154
158
 
155
159
  return (
156
160
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
157
161
  Object.fromEntries(
158
- Object.entries(record).filter(([k, _v]) => keysSet.has(k)),
162
+ ownEnumerableEntries(record).filter(([k]) => keysSet.has(k)),
159
163
  ) as never
160
164
  );
161
165
  }
@@ -177,6 +181,10 @@ export namespace Obj {
177
181
  * allowed, and the return type precisely reflects which properties remain
178
182
  * after omission.
179
183
  *
184
+ * **Key handling**: a numeric key `1` matches the property `'1'`, and
185
+ * symbol-keyed properties are kept unless omitted, like string-keyed ones.
186
+ * Only own enumerable properties are copied.
187
+ *
180
188
  * @example
181
189
  *
182
190
  * ```ts
@@ -238,12 +246,12 @@ export namespace Obj {
238
246
  case 2: {
239
247
  const [record, keys] = args;
240
248
 
241
- const keysSet = new Set<keyof R>(keys);
249
+ const keysSet = toOwnKeySet(keys);
242
250
 
243
251
  return (
244
252
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
245
253
  Object.fromEntries(
246
- Object.entries(record).filter(([k, _v]) => !keysSet.has(k)),
254
+ ownEnumerableEntries(record).filter(([k]) => !keysSet.has(k)),
247
255
  ) as never
248
256
  );
249
257
  }
@@ -337,8 +345,8 @@ export namespace Obj {
337
345
  * This is what `Object.fromEntries(Object.entries(record).map(...))` cannot
338
346
  * express: an entries array carries no evidence that it still covers every
339
347
  * key of the record it came from, so its element type is all the return type
340
- * can be built from. Going through a mapped type instead states the
341
- * invariant that a value-only transform preserves the key set.
348
+ * can be built from. Going through a mapped type instead states the invariant
349
+ * that a value-only transform preserves the key set.
342
350
  *
343
351
  * **Key handling**: keys reach the callback the way `Object.entries` yields
344
352
  * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are
@@ -996,3 +1004,23 @@ declare namespace TsDataForgeInternals {
996
1004
  ? MergeAll<Rest, MergeTwo<Acc, First>>
997
1005
  : Acc;
998
1006
  }
1007
+
1008
+ /**
1009
+ * The keys as `Reflect.ownKeys` reports them: a numeric key `1` becomes `'1'`,
1010
+ * a symbol stays a symbol.
1011
+ */
1012
+ const toOwnKeySet = (
1013
+ keys: readonly PropertyKey[],
1014
+ ): ReadonlySet<string | symbol> =>
1015
+ new Set(keys.map((k) => (typeof k === 'number' ? k.toString() : k)));
1016
+
1017
+ /**
1018
+ * The own enumerable entries of `record`, string and symbol keys alike, in
1019
+ * `Reflect.ownKeys` order. `Object.entries` would leave out the symbols.
1020
+ */
1021
+ const ownEnumerableEntries = (
1022
+ record: UnknownRecord,
1023
+ ): readonly (readonly [string | symbol, unknown])[] =>
1024
+ Reflect.ownKeys(record)
1025
+ .filter((k) => Object.prototype.propertyIsEnumerable.call(record, k))
1026
+ .map((k) => [k, Reflect.get(record, k)] as const);
@@ -89,6 +89,32 @@ describe('pick', () => {
89
89
 
90
90
  assert.deepStrictEqual(result, { name: 'Alice' });
91
91
  });
92
+
93
+ test('pick matches a numeric key', () => {
94
+ const record = { 1: 'a', b: 2 } as const;
95
+
96
+ const result = Obj.pick(record, [1]);
97
+
98
+ expectType<typeof result, Readonly<{ 1: 'a' }>>('=');
99
+
100
+ assert.deepStrictEqual(result, { 1: 'a' });
101
+
102
+ assert.deepStrictEqual(Obj.pick([1])(record), { 1: 'a' });
103
+ });
104
+
105
+ test('pick matches a symbol key', () => {
106
+ const sym = Symbol('s');
107
+
108
+ const record = { [sym]: 'a', b: 2 } as const;
109
+
110
+ const result = Obj.pick(record, [sym]);
111
+
112
+ assert.deepStrictEqual(result, { [sym]: 'a' });
113
+
114
+ assert.deepStrictEqual(Obj.pick([sym])(record), { [sym]: 'a' });
115
+
116
+ assert.deepStrictEqual(Obj.pick(record, ['b']), { b: 2 });
117
+ });
92
118
  });
93
119
 
94
120
  describe('omit', () => {
@@ -132,6 +158,35 @@ describe('omit', () => {
132
158
  assert.deepStrictEqual(result, original);
133
159
  });
134
160
 
161
+ test('omit matches a numeric key', () => {
162
+ const record = { 1: 'a', b: 2 } as const;
163
+
164
+ const result = Obj.omit(record, [1]);
165
+
166
+ expectType<typeof result, Readonly<{ b: 2 }>>('=');
167
+
168
+ assert.deepStrictEqual(result, { b: 2 });
169
+
170
+ assert.deepStrictEqual(Obj.omit([1])(record), { b: 2 });
171
+ });
172
+
173
+ test('omit keeps the symbol keys it is not asked to omit', () => {
174
+ const sym = Symbol('s');
175
+
176
+ const other = Symbol('other');
177
+
178
+ const record = { [sym]: 'a', [other]: 'o', b: 2 } as const;
179
+
180
+ assert.deepStrictEqual(Obj.omit(record, [sym]), { [other]: 'o', b: 2 });
181
+
182
+ assert.deepStrictEqual(Obj.omit([sym])(record), { [other]: 'o', b: 2 });
183
+
184
+ assert.deepStrictEqual(Obj.omit(record, ['b']), {
185
+ [sym]: 'a',
186
+ [other]: 'o',
187
+ });
188
+ });
189
+
135
190
  test('should omit multiple keys in curried form', () => {
136
191
  const omitBAndD = Obj.omit(['b', 'd']);
137
192
 
@@ -67,12 +67,20 @@ export const fastDeepEqual = <T,>(a: T, b: T): boolean => {
67
67
  return isRegExp(b) && a.source === b.source && a.flags === b.flags;
68
68
  }
69
69
 
70
- if (a.valueOf !== Object.prototype.valueOf) {
71
- return a.valueOf() === b.valueOf();
70
+ // A null-prototype object (`Object.create(null)`, `Object.groupBy`) has
71
+ // neither method, so it falls through to the key comparison.
72
+ if (
73
+ typeof a.valueOf === 'function' &&
74
+ a.valueOf !== Object.prototype.valueOf
75
+ ) {
76
+ return typeof b.valueOf === 'function' && a.valueOf() === b.valueOf();
72
77
  }
73
78
 
74
- if (a.toString !== Object.prototype.toString) {
75
- return a.toString() === b.toString();
79
+ if (
80
+ typeof a.toString === 'function' &&
81
+ a.toString !== Object.prototype.toString
82
+ ) {
83
+ return typeof b.toString === 'function' && a.toString() === b.toString();
76
84
  }
77
85
 
78
86
  const aKeys: readonly string[] = Object.keys(a);
@@ -84,12 +92,11 @@ export const fastDeepEqual = <T,>(a: T, b: T): boolean => {
84
92
  aKeys.every(
85
93
  (key) =>
86
94
  hasKey(b, key) &&
87
- // React-specific: avoid traversing React elements' _owner.
88
- // _owner contains circular references
89
- // and is not needed when comparing the actual elements (and not their owners)
90
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
91
- !(key === '_owner' && a['$$typeof']) &&
92
- fastDeepEqual(a[key], b[key]),
95
+ // React-specific: a React element (marked by a symbol `$$typeof`)
96
+ // has its _owner skipped, i.e. treated as equal. It holds circular
97
+ // references and is not part of what the element renders.
98
+ ((key === '_owner' && typeof a['$$typeof'] === 'symbol') ||
99
+ fastDeepEqual(a[key], b[key])),
93
100
  )
94
101
  );
95
102
  }