ts-data-forge 14.0.1 → 14.1.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 (81) hide show
  1. package/README.md +5 -5
  2. package/dist/array/impl/array-utils-transformation.d.mts +4 -1
  3. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  4. package/dist/array/impl/array-utils-transformation.mjs +4 -5
  5. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  6. package/dist/array/impl/array-utils-validation.d.mts +8 -3
  7. package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
  8. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  9. package/dist/collections/imap-mapped.mjs +1 -1
  10. package/dist/collections/imap-mapped.mjs.map +1 -1
  11. package/dist/collections/imap.mjs +1 -1
  12. package/dist/collections/imap.mjs.map +1 -1
  13. package/dist/functional/match.d.mts.map +1 -1
  14. package/dist/functional/match.mjs.map +1 -1
  15. package/dist/number/branded-types/int.d.mts +1 -1
  16. package/dist/number/branded-types/int16.d.mts +1 -1
  17. package/dist/number/branded-types/int32.d.mts +1 -1
  18. package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
  19. package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
  20. package/dist/number/branded-types/non-positive-finite-number.mjs +1 -1
  21. package/dist/number/branded-types/non-positive-finite-number.mjs.map +1 -1
  22. package/dist/number/branded-types/positive-int.d.mts +1 -1
  23. package/dist/number/branded-types/positive-int16.d.mts +1 -1
  24. package/dist/number/branded-types/positive-int32.d.mts +1 -1
  25. package/dist/number/branded-types/positive-safe-int.d.mts +1 -1
  26. package/dist/number/branded-types/positive-uint16.d.mts +1 -1
  27. package/dist/number/branded-types/positive-uint32.d.mts +1 -1
  28. package/dist/number/branded-types/safe-int.d.mts +1 -1
  29. package/dist/number/branded-types/safe-uint.d.mts +1 -1
  30. package/dist/number/branded-types/uint.d.mts +1 -1
  31. package/dist/number/branded-types/uint16.d.mts +1 -1
  32. package/dist/number/branded-types/uint32.d.mts +1 -1
  33. package/dist/number/enum/int8.d.mts +2 -2
  34. package/dist/number/enum/int8.d.mts.map +1 -1
  35. package/dist/number/enum/int8.mjs.map +1 -1
  36. package/dist/number/enum/uint8.d.mts +2 -2
  37. package/dist/number/enum/uint8.d.mts.map +1 -1
  38. package/dist/number/enum/uint8.mjs.map +1 -1
  39. package/dist/number/num-arithmetic-types.d.mts +8 -9
  40. package/dist/number/num-arithmetic-types.d.mts.map +1 -1
  41. package/dist/number/refined-number-utils.d.mts +2 -2
  42. package/dist/number/refined-number-utils.d.mts.map +1 -1
  43. package/dist/number/refined-number-utils.mjs.map +1 -1
  44. package/dist/object/object.d.mts +12 -12
  45. package/dist/object/object.d.mts.map +1 -1
  46. package/dist/object/object.mjs +5 -1
  47. package/dist/object/object.mjs.map +1 -1
  48. package/dist/others/fast-deep-equal.d.mts.map +1 -1
  49. package/dist/others/fast-deep-equal.mjs +2 -2
  50. package/dist/others/fast-deep-equal.mjs.map +1 -1
  51. package/package.json +2 -2
  52. package/src/array/impl/array-utils-modification.test.mts +26 -5
  53. package/src/array/impl/array-utils-shape-invariants.test.mts +259 -17
  54. package/src/array/impl/array-utils-slicing.test.mts +11 -3
  55. package/src/array/impl/array-utils-transformation.mts +41 -18
  56. package/src/array/impl/array-utils-transformation.test.mts +10 -3
  57. package/src/array/impl/array-utils-validation.mts +54 -10
  58. package/src/array/impl/array-utils-validation.test.mts +153 -3
  59. package/src/collections/imap-mapped.mts +1 -1
  60. package/src/collections/imap-mapped.test.mts +2 -2
  61. package/src/collections/imap.mts +1 -1
  62. package/src/collections/iset-mapped.test.mts +1 -1
  63. package/src/functional/match.mts +5 -4
  64. package/src/functional/result.test.mts +5 -2
  65. package/src/guard/is-primitive.test.mts +1 -1
  66. package/src/guard/is-record.test.mts +4 -4
  67. package/src/guard/is-type.test.mts +2 -2
  68. package/src/number/branded-types/finite-number.test.mts +2 -2
  69. package/src/number/branded-types/non-negative-finite-number.test.mts +2 -2
  70. package/src/number/branded-types/non-positive-finite-number.mts +1 -1
  71. package/src/number/branded-types/non-positive-finite-number.test.mts +5 -9
  72. package/src/number/branded-types/non-zero-finite-number.test.mts +2 -2
  73. package/src/number/branded-types/positive-finite-number.test.mts +2 -2
  74. package/src/number/enum/int8.mts +2 -1
  75. package/src/number/enum/uint8.mts +6 -2
  76. package/src/number/num-arithmetic-types.mts +125 -24
  77. package/src/number/num.test.mts +9 -9
  78. package/src/number/refined-number-utils.mts +2 -1
  79. package/src/object/object.mts +29 -17
  80. package/src/object/object.test.mts +2 -1
  81. package/src/others/fast-deep-equal.mts +6 -4
@@ -1,5 +1,6 @@
1
1
  import { type MinLengthArray } from 'ts-type-forge';
2
2
  import { expectType } from '../../expect-type.mjs';
3
+ import { asMinLengthArray } from './array-utils-length-bounded-array-cast.mjs';
3
4
  import {
4
5
  butLast,
5
6
  skip,
@@ -224,9 +225,9 @@ describe('Arr slicing', () => {
224
225
  /* Length-constrained inputs. `List.*` reports what it can see from a
225
226
  fixed-length tuple, and a branded array's `length` is `number`, so it handed
226
227
  the input type straight back — `tail` of an "at least 5" array claimed the
227
- four-element result was still at least five. Never called; see the note in
228
- `array-utils-modification.test.mts`. */
229
- export const slicingTypeChecks = (
228
+ four-element result was still at least five. Shape-per-parameter; see the
229
+ note in `array-utils-modification.test.mts`. */
230
+ const slicingTypeChecks = (
230
231
  brandedFive: MinLengthArray<5, string>,
231
232
  brandedAndTuple: MinLengthArray<3, number> & readonly [1, 2, 3, 4, 5],
232
233
  ): void => {
@@ -274,3 +275,10 @@ export const slicingTypeChecks = (
274
275
 
275
276
  expectType<typeof _takenTuple, readonly [1, 2]>('=');
276
277
  };
278
+
279
+ test('slicing type-level checks', () => {
280
+ slicingTypeChecks(
281
+ asMinLengthArray(5, ['a', 'b', 'c', 'd', 'e'] as const),
282
+ asMinLengthArray(3, [1, 2, 3, 4, 5] as const),
283
+ );
284
+ });
@@ -245,28 +245,51 @@ export const toReversed = <const Ar extends readonly unknown[]>(
245
245
  * assert.deepStrictEqual(alphabetical, expectedWords);
246
246
  * ```
247
247
  */
248
- export const toSorted = <const Ar extends readonly unknown[]>(
249
- ...[array, comparator]: Ar extends readonly number[]
250
- ? readonly [
251
- array: Ar,
252
- // If the array elements are mapped to numbers, comparator is optional.
253
- comparator?: (x: Ar[number], y: Ar[number]) => number,
254
- ]
255
- : readonly [array: Ar, comparator: (x: Ar[number], y: Ar[number]) => number]
256
- ): HasLengthConstraint<Ar> extends true
257
- ? ChangeArrayElement<Ar, Ar[number]>
258
- : IsFixedLengthList<Ar> extends true
259
- ? FixedLengthTuple<Ar['length'], Ar[number]>
260
- : Ar extends NonEmptyTuple<unknown>
261
- ? NonEmptyArray<Ar[number]>
262
- : readonly Ar[number][] =>
263
- // eslint-disable-next-line total-functions/no-unsafe-type-assertion
264
- array.toSorted(
248
+ // Numbers sort without a comparator, so it is optional for them and required
249
+ // for everything else.
250
+ //
251
+ // Stated as two overloads rather than one conditional rest-parameter tuple.
252
+ // The conditional form reads well, but a *generic* `Ar` cannot decide it, so
253
+ // the parameter list stayed unresolved and `Arr.toSorted(xs, cmp)` inside a
254
+ // function that is itself generic over the array was rejected outright:
255
+ //
256
+ // Argument of type '[T, (a: number, b: number) => number]' is not
257
+ // assignable to parameter of type 'T extends readonly number[] ? ... : ...'
258
+ //
259
+ // Overload resolution picks a branch per call instead, so both concrete and
260
+ // generic callers work. The *return* type stays conditional — that one a
261
+ // generic caller handles, as `toSortedBy` (same shape) already showed.
262
+ export function toSorted<const Ar extends readonly number[]>(
263
+ array: Ar,
264
+ comparator?: (x: Ar[number], y: Ar[number]) => number,
265
+ ): SortedResult<Ar>;
266
+
267
+ export function toSorted<const Ar extends readonly unknown[]>(
268
+ array: Ar,
269
+ comparator: (x: Ar[number], y: Ar[number]) => number,
270
+ ): SortedResult<Ar>;
271
+
272
+ export function toSorted(
273
+ array: readonly unknown[],
274
+ comparator?: (x: never, y: never) => number,
275
+ ): readonly unknown[] {
276
+ return array.toSorted(
265
277
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
266
278
  (comparator as ((x: unknown, y: unknown) => number) | undefined) ??
267
279
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
268
280
  ((x, y) => (x as number) - (y as number)),
269
- ) as never;
281
+ );
282
+ }
283
+
284
+ /** The result of sorting `Ar`: same elements, same length, same bounds. */
285
+ type SortedResult<Ar extends readonly unknown[]> =
286
+ HasLengthConstraint<Ar> extends true
287
+ ? ChangeArrayElement<Ar, Ar[number]>
288
+ : IsFixedLengthList<Ar> extends true
289
+ ? FixedLengthTuple<Ar['length'], Ar[number]>
290
+ : Ar extends NonEmptyTuple<unknown>
291
+ ? NonEmptyArray<Ar[number]>
292
+ : readonly Ar[number][];
270
293
 
271
294
  /**
272
295
  * Sorts an array by a mapped value.
@@ -1207,9 +1207,9 @@ describe('Arr transformations', () => {
1207
1207
 
1208
1208
  /* `toReversed` is a known permutation, so the tuple half can be reversed
1209
1209
  alongside the brand. It used to fall back to `ChangeArrayElement`, which kept
1210
- the length but collapsed the positions to their union. Never called; see the
1211
- note in `array-utils-modification.test.mts`. */
1212
- export const reversedTypeChecks = (
1210
+ the length but collapsed the positions to their union. Shape-per-parameter;
1211
+ see the note in `array-utils-modification.test.mts`. */
1212
+ const reversedTypeChecks = (
1213
1213
  brandedFive: MinLengthArray<5, string>,
1214
1214
  brandedAndTuple: MinLengthArray<3, number> & readonly [1, 2, 3, 4, 5],
1215
1215
  ): void => {
@@ -1235,3 +1235,10 @@ export const reversedTypeChecks = (
1235
1235
 
1236
1236
  expectType<typeof _sortedTuple, FixedLengthTuple<3, 1 | 2 | 3>>('~=');
1237
1237
  };
1238
+
1239
+ test('toReversed / toSorted type-level checks', () => {
1240
+ reversedTypeChecks(
1241
+ asMinLengthArray(5, ['a', 'b', 'c', 'd', 'e'] as const),
1242
+ asMinLengthArray(3, [1, 2, 3, 4, 5] as const),
1243
+ );
1244
+ });
@@ -1,4 +1,10 @@
1
- import { type BoolOr, type IsAny, type IsUnknown } from 'ts-type-forge';
1
+ import {
2
+ type BoolOr,
3
+ type ChangeArrayElement,
4
+ type IsAny,
5
+ type IsUnknown,
6
+ type UnknownBrand,
7
+ } from 'ts-type-forge';
2
8
  import { asUint32, Num } from '../../number/index.mjs';
3
9
  import { type ArrayIndex, type SizeType } from '../../types.mjs';
4
10
 
@@ -55,15 +61,30 @@ type Cast<A, B> = A extends B ? A : never;
55
61
  * assert.isFalse(allStartWithA);
56
62
  * ```
57
63
  */
58
- // Type guard overloads - narrow the entire array type
59
- export function every<E, S extends E>(
60
- array: readonly E[],
61
- predicate: (a: E, index: SizeType.Arr) => a is S,
62
- ): array is readonly S[];
63
-
64
- export function every<E, S extends E>(
65
- predicate: (a: E, index: SizeType.Arr) => a is S,
66
- ): (array: readonly E[]) => array is readonly S[];
64
+ // Type guard overloads - narrow the entire array type.
65
+ //
66
+ // Both narrowed types are intersected with `Ar`: a type predicate's type has
67
+ // to be assignable to the parameter's type, and neither the deferred
68
+ // conditional `ChangeArrayElement<Ar, S>` nor the mapped type over a generic
69
+ // `Ar` is (TS2677). The intersection also keeps whatever the caller already
70
+ // knew about the array — a branded five-tuple stays a five-tuple.
71
+ export function every<
72
+ const Ar extends readonly unknown[] & UnknownBrand,
73
+ S extends Ar[number],
74
+ >(
75
+ array: Ar,
76
+ predicate: (a: Ar[number], index: ArrayIndex<Ar>) => a is S,
77
+ ): array is ChangeArrayElement<Ar, S> & Ar;
78
+
79
+ // A plain array or tuple carries no length brand, so the homomorphic mapping
80
+ // is the whole answer — same reasoning as in `map`.
81
+ export function every<
82
+ const Ar extends readonly unknown[],
83
+ S extends Ar[number],
84
+ >(
85
+ array: Ar,
86
+ predicate: (a: Ar[number], index: ArrayIndex<Ar>) => a is S,
87
+ ): array is Readonly<{ [K in keyof Ar]: S }> & Ar;
67
88
 
68
89
  // Regular boolean predicate overloads
69
90
  export function every<const Ar extends readonly unknown[]>(
@@ -71,6 +92,29 @@ export function every<const Ar extends readonly unknown[]>(
71
92
  predicate: (a: Ar[number], index: ArrayIndex<Ar>) => boolean,
72
93
  ): boolean;
73
94
 
95
+ // curried version
96
+ //
97
+ // As in `map`, the branded and the plain case have to be overloads *of the
98
+ // returned function*, not two overloads of `every` itself: both curried
99
+ // signatures take the same single predicate, so overload resolution would
100
+ // always pick the first and the second would be dead — which is what left a
101
+ // plain `readonly unknown[]` unable to satisfy the `UnknownBrand` constraint
102
+ // at the call site of the returned guard.
103
+ //
104
+ // The intersection is also what keeps the group compatible with the
105
+ // implementation signature. A lone `<Ar extends readonly E[] & UnknownBrand>`
106
+ // signature is not something `(array: readonly E[]) => boolean` can implement
107
+ // (TS2394); the unbranded member of the intersection is, and one matching
108
+ // member is enough.
109
+ export function every<E, S extends E>(
110
+ predicate: (a: E, index: SizeType.Arr) => a is S,
111
+ ): (<const Ar extends readonly E[] & UnknownBrand>(
112
+ array: Ar,
113
+ ) => array is ChangeArrayElement<Ar, S> & Ar) &
114
+ (<const Ar extends readonly E[]>(
115
+ array: Ar,
116
+ ) => array is Readonly<{ [K in keyof Ar]: S }> & Ar);
117
+
74
118
  export function every<E>(
75
119
  predicate: (a: E, index: SizeType.Arr) => boolean,
76
120
  ): (array: readonly E[]) => boolean;
@@ -1,4 +1,16 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type FixedLengthTuple,
5
+ type MinLengthArray,
6
+ type ReadonlyRecord,
7
+ } from 'ts-type-forge';
1
8
  import { expectType } from '../../expect-type.mjs';
9
+ import {
10
+ asBoundedLengthArray,
11
+ asFixedLengthArray,
12
+ asMinLengthArray,
13
+ } from './array-utils-length-bounded-array-cast.mjs';
2
14
  import {
3
15
  every,
4
16
  indexIsInRange,
@@ -294,13 +306,12 @@ describe('Arr validations', () => {
294
306
  type ComplexUnion =
295
307
  | Readonly<
296
308
  | { type: 'array'; data: readonly string[] }
297
- | { type: 'object'; data: Record<string, unknown> }
309
+ | { type: 'object'; data: ReadonlyRecord<string, unknown> }
298
310
  >
299
311
  | readonly number[]
300
312
  | string
301
313
  | null;
302
314
 
303
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
304
315
  const processComplex = (value: ComplexUnion): number => {
305
316
  if (isArray(value)) {
306
317
  expectType<typeof value, readonly number[]>('=');
@@ -324,7 +335,7 @@ describe('Arr validations', () => {
324
335
  typeof value,
325
336
  Readonly<
326
337
  | { type: 'array'; data: readonly string[] }
327
- | { type: 'object'; data: Record<string, unknown> }
338
+ | { type: 'object'; data: ReadonlyRecord<string, unknown> }
328
339
  >
329
340
  >('=');
330
341
 
@@ -436,6 +447,145 @@ describe('Arr validations', () => {
436
447
 
437
448
  assert.isTrue(indexMatchesValue);
438
449
  });
450
+
451
+ /* What `every` reports about the array it narrowed, across the shapes the
452
+ two type-guard overloads have to cope with. It used to answer a bare
453
+ `readonly S[]` for every one of them, dropping both the tuple length and
454
+ the length-constraint brand. `assert.isTrue` narrows for the rest of the
455
+ block, so each case checks the runtime answer and the narrowed type
456
+ together. */
457
+ describe('narrowing', () => {
458
+ const isString = (x: string | number): x is string =>
459
+ typeof x === 'string';
460
+
461
+ const values: readonly (string | number)[] = [
462
+ 'Ada',
463
+ 'Grace',
464
+ 'Katherine',
465
+ ] as const;
466
+
467
+ test('should keep a plain array as a plain array', () => {
468
+ const shapeArray: readonly (string | number)[] = values;
469
+
470
+ assert.isTrue(every(shapeArray, isString));
471
+
472
+ expectType<typeof shapeArray, readonly string[]>('~=');
473
+ });
474
+
475
+ test('should keep the length of a tuple', () => {
476
+ const shapeTuple: FixedLengthTuple<2, string | number> = [
477
+ 'Ada',
478
+ 'Grace',
479
+ ] as const;
480
+
481
+ assert.isTrue(every(shapeTuple, isString));
482
+
483
+ expectType<typeof shapeTuple, FixedLengthTuple<2, string>>('~=');
484
+
485
+ expectType<(typeof shapeTuple)['length'], 2>('=');
486
+ });
487
+
488
+ test('should keep a MinLengthArray brand', () => {
489
+ const shapeBranded = asMinLengthArray(3, values);
490
+
491
+ assert.isTrue(every(shapeBranded, isString));
492
+
493
+ expectType<typeof shapeBranded, MinLengthArray<3, string>>('~=');
494
+
495
+ // The brand still rules out `undefined` below the minimum length.
496
+ expectType<(typeof shapeBranded)[0], string>('=');
497
+ });
498
+
499
+ test('should keep a BoundedLengthArray brand', () => {
500
+ const shapeBounded = asBoundedLengthArray(2, 5, values);
501
+
502
+ assert.isTrue(every(shapeBounded, isString));
503
+
504
+ expectType<typeof shapeBounded, BoundedLengthArray<2, 5, string>>('~=');
505
+ });
506
+
507
+ test('should keep a FixedLengthArray brand', () => {
508
+ const shapeFixed = asFixedLengthArray(3, values);
509
+
510
+ assert.isTrue(every(shapeFixed, isString));
511
+
512
+ expectType<typeof shapeFixed, FixedLengthArray<3, string>>('~=');
513
+
514
+ expectType<(typeof shapeFixed)['length'], 3>('=');
515
+ });
516
+
517
+ /* Brand intersected with an exact tuple — the shape the `as*` casts and
518
+ the `is*` guards produce. Brand, length and positions all survive. */
519
+ test('should keep brand, length and positions of a branded tuple', () => {
520
+ const tuple: readonly [string, string | number] = [
521
+ 'Ada',
522
+ 'Grace',
523
+ ] as const;
524
+
525
+ const shapeBrandedTuple = asMinLengthArray(2, tuple);
526
+
527
+ assert.isTrue(every(shapeBrandedTuple, isString));
528
+
529
+ expectType<(typeof shapeBrandedTuple)[0], string>('=');
530
+
531
+ expectType<(typeof shapeBrandedTuple)[1], string>('=');
532
+
533
+ expectType<(typeof shapeBrandedTuple)['length'], 2>('=');
534
+ });
535
+
536
+ /* The curried form has to reach the same conclusions. Its two cases are
537
+ overloads of the *returned* guard rather than of `every` itself, so a
538
+ single `every(predicate)` value serves branded and unbranded arrays
539
+ alike — spelled as two overloads of `every`, the branded one shadowed
540
+ the other and passing a plain array to the result was an error. */
541
+ describe('curried', () => {
542
+ const allStrings = every(isString);
543
+
544
+ test('should keep a plain array as a plain array', () => {
545
+ const shapeArray: readonly (string | number)[] = values;
546
+
547
+ assert.isTrue(allStrings(shapeArray));
548
+
549
+ expectType<typeof shapeArray, readonly string[]>('~=');
550
+ });
551
+
552
+ test('should keep the length of a tuple', () => {
553
+ const shapeTuple: FixedLengthTuple<2, string | number> = [
554
+ 'Ada',
555
+ 'Grace',
556
+ ] as const;
557
+
558
+ assert.isTrue(allStrings(shapeTuple));
559
+
560
+ expectType<typeof shapeTuple, FixedLengthTuple<2, string>>('~=');
561
+
562
+ expectType<(typeof shapeTuple)['length'], 2>('=');
563
+ });
564
+
565
+ test('should keep a MinLengthArray brand', () => {
566
+ const shapeBranded = asMinLengthArray(3, values);
567
+
568
+ assert.isTrue(allStrings(shapeBranded));
569
+
570
+ expectType<typeof shapeBranded, MinLengthArray<3, string>>('~=');
571
+ });
572
+
573
+ test('should keep brand, length and positions of a branded tuple', () => {
574
+ const tuple: readonly [string, string | number] = [
575
+ 'Ada',
576
+ 'Grace',
577
+ ] as const;
578
+
579
+ const shapeBrandedTuple = asMinLengthArray(2, tuple);
580
+
581
+ assert.isTrue(allStrings(shapeBrandedTuple));
582
+
583
+ expectType<(typeof shapeBrandedTuple)[0], string>('=');
584
+
585
+ expectType<(typeof shapeBrandedTuple)['length'], 2>('=');
586
+ });
587
+ });
588
+ });
439
589
  });
440
590
 
441
591
  describe(some, () => {
@@ -1157,7 +1157,7 @@ class IMapMappedClass<K, V, KM extends MapSetKeyType>
1157
1157
  case 'update': {
1158
1158
  const curr = mut_result.get(key);
1159
1159
 
1160
- if (!mut_result.has(key) || curr === undefined) {
1160
+ if (curr === undefined || !mut_result.has(key)) {
1161
1161
  if (this.#showNotFoundMessage) {
1162
1162
  console.warn(
1163
1163
  `IMapMapped.withMutations::update: key not found: ${String(key)}`,
@@ -13,7 +13,7 @@ const testKeyToString = (key: Readonly<TestKey>): string =>
13
13
  `${key.type}_${key.id}` as const;
14
14
 
15
15
  const stringToTestKey = (str: string): TestKey => {
16
- const [type, idStr] = str.split('_');
16
+ const [type, idStr] = str.split('_', 2);
17
17
 
18
18
  return {
19
19
  type: type ?? '',
@@ -113,7 +113,7 @@ describe('IMapMapped.create', () => {
113
113
  ): string => `${key.nested.id}_${key.arr.join(',')}` as const;
114
114
 
115
115
  const stringToComplexKey = (str: string): ComplexKey => {
116
- const [idStr, arrStr] = str.split('_');
116
+ const [idStr, arrStr] = str.split('_', 2);
117
117
 
118
118
  return {
119
119
  nested: {
@@ -877,7 +877,7 @@ class IMapClass<K extends MapSetKeyType, V>
877
877
 
878
878
  const curr = mut_result.get(key);
879
879
 
880
- if (!mut_result.has(key) || curr === undefined) {
880
+ if (curr === undefined || !mut_result.has(key)) {
881
881
  if (this.#showNotFoundMessage) {
882
882
  const keyStr = unknownToString(key);
883
883
 
@@ -13,7 +13,7 @@ const testElementToString = (el: Readonly<TestElement>): string =>
13
13
  `${el.type}_${el.id}` as const;
14
14
 
15
15
  const stringToTestElement = (str: string): TestElement => {
16
- const [type, idStr] = str.split('_');
16
+ const [type, idStr] = str.split('_', 2);
17
17
 
18
18
  return {
19
19
  type: type ?? '',
@@ -121,7 +121,7 @@ expectType<AllCasesCovered<'a' | 'b', Readonly<{ a: 1; b: 2; c: 3 }>>, false>(
121
121
  '=',
122
122
  );
123
123
 
124
- expectType<AllCasesCovered<string, Record<string, string>>, true>('=');
124
+ expectType<AllCasesCovered<string, ReadonlyRecord<string, string>>, true>('=');
125
125
 
126
126
  /**
127
127
  * @template Case A union of string literal types.
@@ -152,9 +152,10 @@ expectType<
152
152
  false
153
153
  >('=');
154
154
 
155
- expectType<IsLiteralUnionFullyCovered<string, Record<string, string>>, false>(
156
- '=',
157
- );
155
+ expectType<
156
+ IsLiteralUnionFullyCovered<string, ReadonlyRecord<string, string>>,
157
+ false
158
+ >('=');
158
159
 
159
160
  expectType<
160
161
  // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
@@ -407,8 +407,11 @@ describe('Result test', () => {
407
407
 
408
408
  describe('fromPromise', () => {
409
409
  test('handles async functions that resolve', async () => {
410
- const asyncFn = async (): Promise<number> =>
411
- Promise.resolve().then(() => 42);
410
+ const asyncFn = async (): Promise<number> => {
411
+ await Promise.resolve();
412
+
413
+ return 42;
414
+ };
412
415
 
413
416
  const result = await Result.fromPromise(asyncFn());
414
417
 
@@ -23,7 +23,7 @@ describe(isPrimitive, () => {
23
23
 
24
24
  assert.isTrue(isPrimitive(0));
25
25
 
26
- assert.isTrue(isPrimitive(-3.14));
26
+ assert.isTrue(isPrimitive(-1.23));
27
27
 
28
28
  assert.isTrue(isPrimitive(Number.NaN));
29
29
 
@@ -1,4 +1,4 @@
1
- import { type UnknownRecord } from 'ts-type-forge';
1
+ import { type ReadonlyRecord, type UnknownRecord } from 'ts-type-forge';
2
2
  import { expectType } from '../expect-type.mjs';
3
3
  import { hasKey } from './has-key.mjs';
4
4
  import { isRecord } from './is-record.mjs';
@@ -31,13 +31,13 @@ describe(isRecord, () => {
31
31
  describe('arrays are not records', () => {
32
32
  test('type-level: arrays are not assignable to UnknownRecord', () => {
33
33
  // Arrays have a number index signature but no string index signature,
34
- // so they are assignable to `Record<number, unknown>` but not to
34
+ // so they are assignable to `ReadonlyRecord<number, unknown>` but not to
35
35
  // `UnknownRecord` (= `ReadonlyRecord<string, unknown>`).
36
- expectType<readonly number[], Record<number, unknown>>('<=');
36
+ expectType<readonly number[], ReadonlyRecord<number, unknown>>('<=');
37
37
 
38
38
  expectType<readonly number[], UnknownRecord>('!<=');
39
39
 
40
- expectType<readonly string[], Record<number, unknown>>('<=');
40
+ expectType<readonly string[], ReadonlyRecord<number, unknown>>('<=');
41
41
 
42
42
  expectType<readonly string[], UnknownRecord>('!<=');
43
43
 
@@ -157,7 +157,7 @@ describe(isNumber, () => {
157
157
 
158
158
  assert.isTrue(isNumber(42));
159
159
 
160
- assert.isTrue(isNumber(-3.14));
160
+ assert.isTrue(isNumber(-1.23));
161
161
 
162
162
  assert.isTrue(isNumber(Number.NaN));
163
163
 
@@ -322,7 +322,7 @@ describe(isNotNumber, () => {
322
322
 
323
323
  assert.isFalse(isNotNumber(42));
324
324
 
325
- assert.isFalse(isNotNumber(-3.14));
325
+ assert.isFalse(isNotNumber(-1.23));
326
326
 
327
327
  assert.isFalse(isNotNumber(Number.NaN));
328
328
 
@@ -12,7 +12,7 @@ describe('FiniteNumber test', () => {
12
12
 
13
13
  expect(() => asFiniteNumber(-1)).not.toThrow();
14
14
 
15
- expect(() => asFiniteNumber(3.14)).not.toThrow();
15
+ expect(() => asFiniteNumber(1.23)).not.toThrow();
16
16
 
17
17
  expect(() => asFiniteNumber(-2.5)).not.toThrow();
18
18
 
@@ -59,7 +59,7 @@ describe('FiniteNumber test', () => {
59
59
 
60
60
  assert.isTrue(FiniteNumber.is(-1));
61
61
 
62
- assert.isTrue(FiniteNumber.is(3.14));
62
+ assert.isTrue(FiniteNumber.is(1.23));
63
63
 
64
64
  assert.isTrue(FiniteNumber.is(-2.5));
65
65
 
@@ -13,7 +13,7 @@ describe('NonNegativeFiniteNumber test', () => {
13
13
 
14
14
  expect(() => asNonNegativeFiniteNumber(1)).not.toThrow();
15
15
 
16
- expect(() => asNonNegativeFiniteNumber(3.14)).not.toThrow();
16
+ expect(() => asNonNegativeFiniteNumber(1.23)).not.toThrow();
17
17
 
18
18
  expect(() => asNonNegativeFiniteNumber(0.5)).not.toThrow();
19
19
 
@@ -71,7 +71,7 @@ describe('NonNegativeFiniteNumber test', () => {
71
71
 
72
72
  assert.isTrue(isNonNegativeFiniteNumber(1));
73
73
 
74
- assert.isTrue(isNonNegativeFiniteNumber(3.14));
74
+ assert.isTrue(isNonNegativeFiniteNumber(1.23));
75
75
 
76
76
  assert.isTrue(isNonNegativeFiniteNumber(0.5));
77
77
 
@@ -23,7 +23,7 @@ const {
23
23
  number,
24
24
  0
25
25
  >({
26
- MIN_VALUE: Number.MAX_VALUE * -1,
26
+ MIN_VALUE: -Number.MAX_VALUE,
27
27
  MAX_VALUE: 0,
28
28
  typeNameInMessage,
29
29
  } as const);
@@ -13,17 +13,13 @@ describe('NonPositiveFiniteNumber test', () => {
13
13
 
14
14
  expect(() => asNonPositiveFiniteNumber(-1)).not.toThrow();
15
15
 
16
- expect(() => asNonPositiveFiniteNumber(-3.14)).not.toThrow();
16
+ expect(() => asNonPositiveFiniteNumber(-1.23)).not.toThrow();
17
17
 
18
18
  expect(() => asNonPositiveFiniteNumber(-0.5)).not.toThrow();
19
19
 
20
- expect(() =>
21
- asNonPositiveFiniteNumber(Number.MIN_VALUE * -1),
22
- ).not.toThrow();
20
+ expect(() => asNonPositiveFiniteNumber(-Number.MIN_VALUE)).not.toThrow();
23
21
 
24
- expect(() =>
25
- asNonPositiveFiniteNumber(Number.MAX_VALUE * -1),
26
- ).not.toThrow();
22
+ expect(() => asNonPositiveFiniteNumber(-Number.MAX_VALUE)).not.toThrow();
27
23
  });
28
24
 
29
25
  test('rejects positive numbers', () => {
@@ -63,11 +59,11 @@ describe('NonPositiveFiniteNumber test', () => {
63
59
 
64
60
  assert.isTrue(isNonPositiveFiniteNumber(-1));
65
61
 
66
- assert.isTrue(isNonPositiveFiniteNumber(-3.14));
62
+ assert.isTrue(isNonPositiveFiniteNumber(-1.23));
67
63
 
68
64
  assert.isTrue(isNonPositiveFiniteNumber(-0.5));
69
65
 
70
- assert.isTrue(isNonPositiveFiniteNumber(Number.MIN_VALUE * -1));
66
+ assert.isTrue(isNonPositiveFiniteNumber(-Number.MIN_VALUE));
71
67
  });
72
68
 
73
69
  test('correctly rejects positive numbers', () => {
@@ -13,7 +13,7 @@ describe('NonZeroFiniteNumber test', () => {
13
13
 
14
14
  expect(() => asNonZeroFiniteNumber(-1)).not.toThrow();
15
15
 
16
- expect(() => asNonZeroFiniteNumber(3.14)).not.toThrow();
16
+ expect(() => asNonZeroFiniteNumber(1.23)).not.toThrow();
17
17
 
18
18
  expect(() => asNonZeroFiniteNumber(-2.5)).not.toThrow();
19
19
 
@@ -73,7 +73,7 @@ describe('NonZeroFiniteNumber test', () => {
73
73
 
74
74
  assert.isTrue(isNonZeroFiniteNumber(-1));
75
75
 
76
- assert.isTrue(isNonZeroFiniteNumber(3.14));
76
+ assert.isTrue(isNonZeroFiniteNumber(1.23));
77
77
 
78
78
  assert.isTrue(isNonZeroFiniteNumber(-2.5));
79
79
 
@@ -11,7 +11,7 @@ describe('PositiveFiniteNumber test', () => {
11
11
  test('accepts valid positive finite numbers', () => {
12
12
  expect(() => asPositiveFiniteNumber(1)).not.toThrow();
13
13
 
14
- expect(() => asPositiveFiniteNumber(3.14)).not.toThrow();
14
+ expect(() => asPositiveFiniteNumber(1.23)).not.toThrow();
15
15
 
16
16
  expect(() => asPositiveFiniteNumber(0.5)).not.toThrow();
17
17
 
@@ -75,7 +75,7 @@ describe('PositiveFiniteNumber test', () => {
75
75
  test('correctly identifies positive finite numbers', () => {
76
76
  assert.isTrue(isPositiveFiniteNumber(1));
77
77
 
78
- assert.isTrue(isPositiveFiniteNumber(3.14));
78
+ assert.isTrue(isPositiveFiniteNumber(1.23));
79
79
 
80
80
  assert.isTrue(isPositiveFiniteNumber(0.5));
81
81