ts-data-forge 6.7.0 → 6.9.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 (60) hide show
  1. package/dist/array/impl/array-utils-set-op.d.mts.map +1 -1
  2. package/dist/array/impl/array-utils-set-op.mjs +1 -3
  3. package/dist/array/impl/array-utils-set-op.mjs.map +1 -1
  4. package/dist/array/impl/array-utils-transformation.d.mts +25 -0
  5. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  6. package/dist/array/impl/array-utils-transformation.mjs +36 -1
  7. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  8. package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
  9. package/dist/array/impl/array-utils-validation.mjs +2 -6
  10. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  11. package/dist/array/impl/index.mjs +1 -1
  12. package/dist/object/object.d.mts +56 -0
  13. package/dist/object/object.d.mts.map +1 -1
  14. package/dist/object/object.mjs +107 -1
  15. package/dist/object/object.mjs.map +1 -1
  16. package/dist/others/fast-deep-equal.d.mts.map +1 -1
  17. package/dist/others/fast-deep-equal.mjs +0 -1
  18. package/dist/others/fast-deep-equal.mjs.map +1 -1
  19. package/package.json +14 -14
  20. package/src/array/impl/array-utils-overload-type-error.test.mts +2 -4
  21. package/src/array/impl/array-utils-set-op.mts +0 -1
  22. package/src/array/impl/array-utils-transformation.mts +40 -0
  23. package/src/array/impl/array-utils-transformation.test.mts +54 -0
  24. package/src/array/impl/array-utils-validation.mts +2 -6
  25. package/src/functional/optional.test.mts +5 -7
  26. package/src/functional/result.test.mts +8 -10
  27. package/src/functional/ternary-result.test.mts +4 -4
  28. package/src/json/json.test.mts +5 -5
  29. package/src/number/branded-types/finite-number.test.mts +11 -15
  30. package/src/number/branded-types/int.test.mts +13 -13
  31. package/src/number/branded-types/int16.test.mts +15 -15
  32. package/src/number/branded-types/int32.test.mts +15 -15
  33. package/src/number/branded-types/non-negative-finite-number.test.mts +15 -19
  34. package/src/number/branded-types/non-negative-int16.test.mts +15 -15
  35. package/src/number/branded-types/non-negative-int32.test.mts +15 -15
  36. package/src/number/branded-types/non-zero-finite-number.test.mts +18 -18
  37. package/src/number/branded-types/non-zero-int.test.mts +14 -18
  38. package/src/number/branded-types/non-zero-int16.test.mts +15 -19
  39. package/src/number/branded-types/non-zero-int32.test.mts +15 -19
  40. package/src/number/branded-types/non-zero-safe-int.test.mts +18 -22
  41. package/src/number/branded-types/non-zero-uint16.test.mts +15 -15
  42. package/src/number/branded-types/non-zero-uint32.test.mts +15 -15
  43. package/src/number/branded-types/positive-finite-number.test.mts +18 -18
  44. package/src/number/branded-types/positive-int.test.mts +16 -22
  45. package/src/number/branded-types/positive-int16.test.mts +14 -14
  46. package/src/number/branded-types/positive-int32.test.mts +14 -14
  47. package/src/number/branded-types/positive-safe-int.test.mts +18 -20
  48. package/src/number/branded-types/positive-uint16.test.mts +15 -15
  49. package/src/number/branded-types/positive-uint32.test.mts +15 -15
  50. package/src/number/branded-types/safe-int.test.mts +17 -21
  51. package/src/number/branded-types/safe-uint.test.mts +16 -22
  52. package/src/number/branded-types/uint.test.mts +14 -14
  53. package/src/number/branded-types/uint16.test.mts +14 -14
  54. package/src/number/branded-types/uint32.test.mts +14 -14
  55. package/src/number/enum/int8.test.mts +1 -1
  56. package/src/number/enum/uint8.test.mts +1 -1
  57. package/src/object/object.mts +174 -1
  58. package/src/object/object.test.mts +172 -0
  59. package/src/others/fast-deep-equal.mts +0 -1
  60. package/src/others/unknown-to-string.test.mts +1 -1
@@ -6,37 +6,37 @@ import { asUint16, isUint16, Uint16 } from './uint16.mjs';
6
6
  describe('Uint16 test', () => {
7
7
  describe(asUint16, () => {
8
8
  test('accepts valid uint16 values', () => {
9
- expect(() => asUint16(0)).not.toThrowError();
9
+ expect(() => asUint16(0)).not.toThrow();
10
10
 
11
- expect(() => asUint16(1)).not.toThrowError();
11
+ expect(() => asUint16(1)).not.toThrow();
12
12
 
13
- expect(() => asUint16(65_535)).not.toThrowError(); // 2^16 - 1
13
+ expect(() => asUint16(65_535)).not.toThrow(); // 2^16 - 1
14
14
 
15
- expect(() => asUint16(32_768)).not.toThrowError(); // 2^15
15
+ expect(() => asUint16(32_768)).not.toThrow(); // 2^15
16
16
  });
17
17
 
18
18
  test('rejects values outside uint16 range', () => {
19
- expect(() => asUint16(65_536)).toThrowError(TypeError); // 2^16
19
+ expect(() => asUint16(65_536)).toThrow(TypeError); // 2^16
20
20
 
21
- expect(() => asUint16(100_000)).toThrowError(TypeError);
21
+ expect(() => asUint16(100_000)).toThrow(TypeError);
22
22
  });
23
23
 
24
24
  test('rejects negative integers', () => {
25
- expect(() => asUint16(-1)).toThrowError(TypeError);
25
+ expect(() => asUint16(-1)).toThrow(TypeError);
26
26
 
27
- expect(() => asUint16(-42)).toThrowError(TypeError);
27
+ expect(() => asUint16(-42)).toThrow(TypeError);
28
28
  });
29
29
 
30
30
  test('rejects non-integers', () => {
31
- expect(() => asUint16(Number.NaN)).toThrowError(TypeError);
31
+ expect(() => asUint16(Number.NaN)).toThrow(TypeError);
32
32
 
33
- expect(() => asUint16(Number.POSITIVE_INFINITY)).toThrowError(TypeError);
33
+ expect(() => asUint16(Number.POSITIVE_INFINITY)).toThrow(TypeError);
34
34
 
35
- expect(() => asUint16(Number.NEGATIVE_INFINITY)).toThrowError(TypeError);
35
+ expect(() => asUint16(Number.NEGATIVE_INFINITY)).toThrow(TypeError);
36
36
 
37
- expect(() => asUint16(1.2)).toThrowError(TypeError);
37
+ expect(() => asUint16(1.2)).toThrow(TypeError);
38
38
 
39
- expect(() => asUint16(-3.4)).toThrowError(TypeError);
39
+ expect(() => asUint16(-3.4)).toThrow(TypeError);
40
40
  });
41
41
 
42
42
  test('returns the same value for valid inputs', () => {
@@ -55,7 +55,7 @@ describe('Uint16 test', () => {
55
55
  { name: '-3.4', value: -3.4 },
56
56
  { name: '-1', value: -1 },
57
57
  ] as const)(`asUint16($name) should throw a TypeError`, ({ value }) => {
58
- expect(() => asUint16(value)).toThrowError(
58
+ expect(() => asUint16(value)).toThrow(
59
59
  new TypeError(
60
60
  `Expected a non-negative integer less than 2^16, got: ${value}`,
61
61
  ),
@@ -6,37 +6,37 @@ import { asUint32, isUint32, Uint32 } from './uint32.mjs';
6
6
  describe('Uint32 test', () => {
7
7
  describe(asUint32, () => {
8
8
  test('accepts valid uint32 values', () => {
9
- expect(() => asUint32(0)).not.toThrowError();
9
+ expect(() => asUint32(0)).not.toThrow();
10
10
 
11
- expect(() => asUint32(1)).not.toThrowError();
11
+ expect(() => asUint32(1)).not.toThrow();
12
12
 
13
- expect(() => asUint32(4_294_967_295)).not.toThrowError(); // 2^32 - 1
13
+ expect(() => asUint32(4_294_967_295)).not.toThrow(); // 2^32 - 1
14
14
 
15
- expect(() => asUint32(2_147_483_648)).not.toThrowError(); // 2^31
15
+ expect(() => asUint32(2_147_483_648)).not.toThrow(); // 2^31
16
16
  });
17
17
 
18
18
  test('rejects values outside uint32 range', () => {
19
- expect(() => asUint32(4_294_967_296)).toThrowError(TypeError); // 2^32
19
+ expect(() => asUint32(4_294_967_296)).toThrow(TypeError); // 2^32
20
20
 
21
- expect(() => asUint32(10_000_000_000)).toThrowError(TypeError);
21
+ expect(() => asUint32(10_000_000_000)).toThrow(TypeError);
22
22
  });
23
23
 
24
24
  test('rejects negative integers', () => {
25
- expect(() => asUint32(-1)).toThrowError(TypeError);
25
+ expect(() => asUint32(-1)).toThrow(TypeError);
26
26
 
27
- expect(() => asUint32(-42)).toThrowError(TypeError);
27
+ expect(() => asUint32(-42)).toThrow(TypeError);
28
28
  });
29
29
 
30
30
  test('rejects non-integers', () => {
31
- expect(() => asUint32(Number.NaN)).toThrowError(TypeError);
31
+ expect(() => asUint32(Number.NaN)).toThrow(TypeError);
32
32
 
33
- expect(() => asUint32(Number.POSITIVE_INFINITY)).toThrowError(TypeError);
33
+ expect(() => asUint32(Number.POSITIVE_INFINITY)).toThrow(TypeError);
34
34
 
35
- expect(() => asUint32(Number.NEGATIVE_INFINITY)).toThrowError(TypeError);
35
+ expect(() => asUint32(Number.NEGATIVE_INFINITY)).toThrow(TypeError);
36
36
 
37
- expect(() => asUint32(1.2)).toThrowError(TypeError);
37
+ expect(() => asUint32(1.2)).toThrow(TypeError);
38
38
 
39
- expect(() => asUint32(-3.4)).toThrowError(TypeError);
39
+ expect(() => asUint32(-3.4)).toThrow(TypeError);
40
40
  });
41
41
 
42
42
  test('returns the same value for valid inputs', () => {
@@ -55,7 +55,7 @@ describe('Uint32 test', () => {
55
55
  { name: '-3.4', value: -3.4 },
56
56
  { name: '-1', value: -1 },
57
57
  ] as const)(`asUint32($name) should throw a TypeError`, ({ value }) => {
58
- expect(() => asUint32(value)).toThrowError(
58
+ expect(() => asUint32(value)).toThrow(
59
59
  new TypeError(
60
60
  `Expected a non-negative integer less than 2^32, got: ${value}`,
61
61
  ),
@@ -35,7 +35,7 @@ describe('Int8 test', () => {
35
35
  { value: Number.POSITIVE_INFINITY, name: 'Infinity' },
36
36
  { value: Number.NEGATIVE_INFINITY, name: '-Infinity' },
37
37
  ])('asInt8($name) should throw TypeError', ({ value }) => {
38
- expect(() => asInt8(value)).toThrowError(TypeError);
38
+ expect(() => asInt8(value)).toThrow(TypeError);
39
39
  });
40
40
  });
41
41
 
@@ -35,7 +35,7 @@ describe('Uint8 test', () => {
35
35
  { value: Number.POSITIVE_INFINITY, name: 'Infinity' },
36
36
  { value: Number.NEGATIVE_INFINITY, name: '-Infinity' },
37
37
  ])('asUint8($name) should throw TypeError', ({ value }) => {
38
- expect(() => asUint8(value)).toThrowError(TypeError);
38
+ expect(() => asUint8(value)).toThrow(TypeError);
39
39
  });
40
40
  });
41
41
 
@@ -1,3 +1,6 @@
1
+ import { Arr } from '../array/index.mjs';
2
+ import { hasKey, isRecord } from '../guard/index.mjs';
3
+
1
4
  /**
2
5
  * A collection of type-safe object utility functions providing functional
3
6
  * programming patterns for object manipulation, including pick, omit, shallow
@@ -58,7 +61,6 @@ export namespace Obj {
58
61
 
59
62
  const bEntries = Object.entries(b);
60
63
 
61
- // eslint-disable-next-line ts-data-forge/prefer-arr-is-array-of-length
62
64
  if (aEntries.length !== bEntries.length) return false;
63
65
 
64
66
  return aEntries.every(([k, v]) => eq(b[k], v));
@@ -311,6 +313,8 @@ export namespace Obj {
311
313
  *
312
314
  * @example
313
315
  *
316
+ * <!-- doc:embed:jsdoc:example:./samples/src/object/merge-example.mts -->
317
+ *
314
318
  * ```ts
315
319
  * const a = { a: 0, b: 0 } as const;
316
320
  * const b = { b: 1, c: 0 } as const;
@@ -320,6 +324,8 @@ export namespace Obj {
320
324
  * assert.deepStrictEqual(result, { a: 0, b: 1, c: 0 });
321
325
  * ```
322
326
  *
327
+ * <!-- /doc:embed:jsdoc:example:./samples/src/object/merge-example.mts -->
328
+ *
323
329
  * @param records - The records to merge
324
330
  * @returns A new record with all properties merged
325
331
  */
@@ -328,8 +334,175 @@ export namespace Obj {
328
334
  ): TsDataForgeInternals.MergeAll<Records> =>
329
335
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
330
336
  Object.fromEntries(records.flatMap((r) => Object.entries(r))) as never;
337
+
338
+ /**
339
+ * Deeply picks a nested property from an object along the specified key path.
340
+ * Supports both direct and curried usage.
341
+ *
342
+ * @example
343
+ *
344
+ * ```ts
345
+ * const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;
346
+ *
347
+ * // Direct usage
348
+ * const result = Obj.deepPick(data, ['a', 'b', 'c']);
349
+ * assert.deepStrictEqual(result, { a: { b: { c: 1 } } });
350
+ *
351
+ * // Curried usage with pipe
352
+ * const pickName = Obj.deepPick(['user', 'name']);
353
+ * const result2 = pipe(data).map(pickName).value;
354
+ * ```
355
+ *
356
+ * @template R - The type of the input record
357
+ * @template Path - The key path tuple
358
+ * @param record - The source record
359
+ * @param path - A readonly tuple of keys representing the nested path
360
+ * @returns A new record containing only the nested property at the path
361
+ */
362
+ export function deepPick<
363
+ const R extends UnknownRecord,
364
+ const Path extends readonly (string | number)[],
365
+ >(record: R, path: Path): DeepPick<R, Path>;
366
+
367
+ // Curried version
368
+ export function deepPick<const Path extends readonly (string | number)[]>(
369
+ path: Path,
370
+ ): <const R extends UnknownRecord>(record: R) => DeepPick<R, Path>;
371
+
372
+ export function deepPick<
373
+ const R extends UnknownRecord,
374
+ const Path extends readonly (string | number)[],
375
+ >(
376
+ ...args: readonly [record: R, path: Path] | readonly [path: Path]
377
+ ): DeepPick<R, Path> | ((record: R) => DeepPick<R, Path>) {
378
+ switch (args.length) {
379
+ case 2: {
380
+ const [record, path] = args;
381
+
382
+ return (
383
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
384
+ deepPickImpl(record, path) as never
385
+ );
386
+ }
387
+
388
+ case 1: {
389
+ const [path] = args;
390
+
391
+ return (record: R) => deepPick(record, path);
392
+ }
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Deeply omits a nested property from an object along the specified key path.
398
+ * Supports both direct and curried usage.
399
+ *
400
+ * @example
401
+ *
402
+ * ```ts
403
+ * const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;
404
+ *
405
+ * // Direct usage
406
+ * const result = Obj.deepOmit(data, ['a', 'b', 'c']);
407
+ * assert.deepStrictEqual(result, { a: { b: { d: 2 }, e: 3 }, f: 4 });
408
+ *
409
+ * // Curried usage with pipe
410
+ * const omitPassword = Obj.deepOmit(['user', 'password']);
411
+ * const result2 = pipe(data).map(omitPassword).value;
412
+ * ```
413
+ *
414
+ * @template R - The type of the input record
415
+ * @template Path - The key path tuple
416
+ * @param record - The source record
417
+ * @param path - A readonly tuple of keys representing the nested path to omit
418
+ * @returns A new record with the nested property at the path removed
419
+ */
420
+ export function deepOmit<
421
+ const R extends UnknownRecord,
422
+ const Path extends readonly (string | number)[],
423
+ >(record: R, path: Path): DeepOmit<R, Path>;
424
+
425
+ // Curried version
426
+ export function deepOmit<const Path extends readonly (string | number)[]>(
427
+ path: Path,
428
+ ): <const R extends UnknownRecord>(record: R) => DeepOmit<R, Path>;
429
+
430
+ export function deepOmit<
431
+ const R extends UnknownRecord,
432
+ const Path extends readonly (string | number)[],
433
+ >(
434
+ ...args: readonly [record: R, path: Path] | readonly [path: Path]
435
+ ): DeepOmit<R, Path> | ((record: R) => DeepOmit<R, Path>) {
436
+ switch (args.length) {
437
+ case 2: {
438
+ const [record, path] = args;
439
+
440
+ return (
441
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
442
+ deepOmitImpl(record, path) as never
443
+ );
444
+ }
445
+
446
+ case 1: {
447
+ const [path] = args;
448
+
449
+ return (record: R) => deepOmit(record, path);
450
+ }
451
+ }
452
+ }
331
453
  }
332
454
 
455
+ const deepPickImpl = (
456
+ record: UnknownRecord,
457
+ path: readonly (string | number)[],
458
+ ): UnknownRecord => {
459
+ if (!Arr.isNonEmpty(path)) return record;
460
+
461
+ const head = path[0];
462
+
463
+ if (!hasKey(record, head)) return {};
464
+
465
+ const value = record[head];
466
+
467
+ const tail = path.slice(1);
468
+
469
+ if (!Arr.isNonEmpty(tail)) return { [head]: value };
470
+
471
+ if (!isRecord(value)) return { [head]: {} };
472
+
473
+ return {
474
+ [head]: deepPickImpl(value, tail),
475
+ };
476
+ };
477
+
478
+ const deepOmitImpl = (
479
+ record: UnknownRecord,
480
+ path: readonly (string | number)[],
481
+ ): UnknownRecord => {
482
+ if (!Arr.isNonEmpty(path)) return record;
483
+
484
+ const head = path[0];
485
+
486
+ const tail = path.slice(1);
487
+
488
+ if (!hasKey(record, head)) return record;
489
+
490
+ if (!Arr.isNonEmpty(tail)) {
491
+ return Object.fromEntries(
492
+ Object.entries(record).filter(([k]) => k !== head),
493
+ );
494
+ }
495
+
496
+ const value = record[head];
497
+
498
+ if (!isRecord(value)) return record;
499
+
500
+ return {
501
+ ...record,
502
+ [head]: deepOmitImpl(value, tail),
503
+ };
504
+ };
505
+
333
506
  /**
334
507
  * @internal
335
508
  * Internal type utilities for the Obj module.
@@ -472,3 +472,175 @@ describe('merge', () => {
472
472
  assert.deepStrictEqual(result, { a: 1, b: 'text', c: true, d: 42 });
473
473
  });
474
474
  });
475
+
476
+ describe('deepPick', () => {
477
+ test('should deeply pick a nested property', () => {
478
+ const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;
479
+
480
+ const result = Obj.deepPick(data, ['a', 'b', 'c']);
481
+
482
+ assert.deepStrictEqual(result, { a: { b: { c: 1 } } });
483
+
484
+ expectType<
485
+ typeof result,
486
+ Readonly<{ a: Readonly<{ b: Readonly<{ c: 1 }> }> }>
487
+ >('=');
488
+ });
489
+
490
+ test('should pick at depth 1', () => {
491
+ const data = { a: 1, b: 2, c: 3 } as const;
492
+
493
+ const result = Obj.deepPick(data, ['a']);
494
+
495
+ assert.deepStrictEqual(result, { a: 1 });
496
+
497
+ expectType<typeof result, Readonly<{ a: 1 }>>('=');
498
+ });
499
+
500
+ test('should pick at depth 2', () => {
501
+ const data = { a: { b: 10, c: 20 }, d: 30 } as const;
502
+
503
+ const result = Obj.deepPick(data, ['a', 'b']);
504
+
505
+ assert.deepStrictEqual(result, { a: { b: 10 } });
506
+
507
+ expectType<typeof result, Readonly<{ a: Readonly<{ b: 10 }> }>>('=');
508
+ });
509
+
510
+ test('should return empty object for non-existent key', () => {
511
+ const data = { a: 1 } as const;
512
+
513
+ const result = Obj.deepPick(data, ['x']);
514
+
515
+ assert.deepStrictEqual(result, {});
516
+ });
517
+
518
+ test('should return empty nested for non-existent nested key', () => {
519
+ const data = { a: { b: 1 } } as const;
520
+
521
+ const result = Obj.deepPick(data, ['a', 'x']);
522
+
523
+ assert.deepStrictEqual(result, { a: {} });
524
+ });
525
+
526
+ test('should support curried form with correct type inference', () => {
527
+ const pickABC = Obj.deepPick(['a', 'b', 'c']);
528
+
529
+ const data = { a: { b: { c: 42, d: 99 } } } as const;
530
+
531
+ const result = pickABC(data);
532
+
533
+ assert.deepStrictEqual(result, { a: { b: { c: 42 } } });
534
+
535
+ expectType<
536
+ typeof result,
537
+ Readonly<{ a: Readonly<{ b: Readonly<{ c: 42 }> }> }>
538
+ >('=');
539
+ });
540
+
541
+ test('should work with pipe when curried with correct type inference', () => {
542
+ const pickNested = Obj.deepPick(['a', 'b']);
543
+
544
+ const data = { a: { b: 1, c: 2 }, d: 3 } as const;
545
+
546
+ const result = pipe(data).map(pickNested).value;
547
+
548
+ assert.deepStrictEqual(result, { a: { b: 1 } });
549
+
550
+ expectType<typeof result, Readonly<{ a: Readonly<{ b: 1 }> }>>('=');
551
+ });
552
+
553
+ test('should return {} for path through primitive', () => {
554
+ const data = { a: 42 } as const;
555
+
556
+ const result = Obj.deepPick(data, ['a', 'b']);
557
+
558
+ assert.deepStrictEqual(result, { a: {} });
559
+ });
560
+ });
561
+
562
+ describe('deepOmit', () => {
563
+ test('should deeply omit a nested property', () => {
564
+ const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;
565
+
566
+ const result = Obj.deepOmit(data, ['a', 'b', 'c']);
567
+
568
+ assert.deepStrictEqual(result, { a: { b: { d: 2 }, e: 3 }, f: 4 });
569
+
570
+ expectType<
571
+ typeof result,
572
+ Readonly<{ a: Readonly<{ b: Readonly<{ d: 2 }>; e: 3 }>; f: 4 }>
573
+ >('=');
574
+ });
575
+
576
+ test('should omit at depth 1', () => {
577
+ const data = { a: 1, b: 2, c: 3 } as const;
578
+
579
+ const result = Obj.deepOmit(data, ['a']);
580
+
581
+ assert.deepStrictEqual(result, { b: 2, c: 3 });
582
+
583
+ expectType<typeof result, Readonly<{ b: 2; c: 3 }>>('=');
584
+ });
585
+
586
+ test('should omit at depth 2', () => {
587
+ const data = { a: { b: 10, c: 20 }, d: 30 } as const;
588
+
589
+ const result = Obj.deepOmit(data, ['a', 'b']);
590
+
591
+ assert.deepStrictEqual(result, { a: { c: 20 }, d: 30 });
592
+
593
+ expectType<typeof result, Readonly<{ a: Readonly<{ c: 20 }>; d: 30 }>>('=');
594
+ });
595
+
596
+ test('should return unchanged for non-existent key', () => {
597
+ const data = { a: 1, b: 2 } as const;
598
+
599
+ const result = Obj.deepOmit(data, ['x']);
600
+
601
+ assert.deepStrictEqual(result, { a: 1, b: 2 });
602
+ });
603
+
604
+ test('should return unchanged for non-existent nested key', () => {
605
+ const data = { a: { b: 1 } } as const;
606
+
607
+ const result = Obj.deepOmit(data, ['a', 'x']);
608
+
609
+ assert.deepStrictEqual(result, { a: { b: 1 } });
610
+ });
611
+
612
+ test('should return unchanged for path through primitive', () => {
613
+ const data = { a: 42, b: 'hello' } as const;
614
+
615
+ const result = Obj.deepOmit(data, ['a', 'toString']);
616
+
617
+ assert.deepStrictEqual(result, { a: 42, b: 'hello' });
618
+ });
619
+
620
+ test('should support curried form with correct type inference', () => {
621
+ const omitABC = Obj.deepOmit(['a', 'b', 'c']);
622
+
623
+ const data = { a: { b: { c: 1, d: 2 } } } as const;
624
+
625
+ const result = omitABC(data);
626
+
627
+ assert.deepStrictEqual(result, { a: { b: { d: 2 } } });
628
+
629
+ expectType<
630
+ typeof result,
631
+ Readonly<{ a: Readonly<{ b: Readonly<{ d: 2 }> }> }>
632
+ >('=');
633
+ });
634
+
635
+ test('should work with pipe when curried with correct type inference', () => {
636
+ const omitNested = Obj.deepOmit(['a', 'b']);
637
+
638
+ const data = { a: { b: 1, c: 2 }, d: 3 } as const;
639
+
640
+ const result = pipe(data).map(omitNested).value;
641
+
642
+ assert.deepStrictEqual(result, { a: { c: 2 }, d: 3 });
643
+
644
+ expectType<typeof result, Readonly<{ a: Readonly<{ c: 2 }>; d: 3 }>>('=');
645
+ });
646
+ });
@@ -1,4 +1,3 @@
1
- /* eslint-disable ts-data-forge/prefer-arr-is-array-of-length */
2
1
  /* eslint-disable ts-data-forge/prefer-arr-is-array */
3
2
 
4
3
  import { isMap, isRegExp, isSet, isTypedArray } from '@sindresorhus/is';
@@ -21,7 +21,7 @@ describe(unknownToString, () => {
21
21
 
22
22
  expect(unknownToString(value)).toBe('123n');
23
23
 
24
- expect(() => JSON.stringify(value)).toThrowError(
24
+ expect(() => JSON.stringify(value)).toThrow(
25
25
  'Do not know how to serialize a BigInt',
26
26
  );
27
27
  });