ts-data-forge 14.0.1 → 14.2.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 (94) 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/functional/optional/impl/optional-map.d.mts +1 -1
  16. package/dist/functional/optional/impl/optional-map.d.mts.map +1 -1
  17. package/dist/functional/optional/impl/optional-map.mjs.map +1 -1
  18. package/dist/functional/result/impl/result-map-err.d.mts +1 -1
  19. package/dist/functional/result/impl/result-map-err.d.mts.map +1 -1
  20. package/dist/functional/result/impl/result-map-err.mjs.map +1 -1
  21. package/dist/functional/result/impl/result-map.d.mts +1 -1
  22. package/dist/functional/result/impl/result-map.d.mts.map +1 -1
  23. package/dist/functional/result/impl/result-map.mjs.map +1 -1
  24. package/dist/number/branded-types/int.d.mts +1 -1
  25. package/dist/number/branded-types/int16.d.mts +1 -1
  26. package/dist/number/branded-types/int32.d.mts +1 -1
  27. package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
  28. package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
  29. package/dist/number/branded-types/non-positive-finite-number.mjs +1 -1
  30. package/dist/number/branded-types/non-positive-finite-number.mjs.map +1 -1
  31. package/dist/number/branded-types/positive-int.d.mts +1 -1
  32. package/dist/number/branded-types/positive-int16.d.mts +1 -1
  33. package/dist/number/branded-types/positive-int32.d.mts +1 -1
  34. package/dist/number/branded-types/positive-safe-int.d.mts +1 -1
  35. package/dist/number/branded-types/positive-uint16.d.mts +1 -1
  36. package/dist/number/branded-types/positive-uint32.d.mts +1 -1
  37. package/dist/number/branded-types/safe-int.d.mts +1 -1
  38. package/dist/number/branded-types/safe-uint.d.mts +1 -1
  39. package/dist/number/branded-types/uint.d.mts +1 -1
  40. package/dist/number/branded-types/uint16.d.mts +1 -1
  41. package/dist/number/branded-types/uint32.d.mts +1 -1
  42. package/dist/number/enum/int8.d.mts +2 -2
  43. package/dist/number/enum/int8.d.mts.map +1 -1
  44. package/dist/number/enum/int8.mjs.map +1 -1
  45. package/dist/number/enum/uint8.d.mts +2 -2
  46. package/dist/number/enum/uint8.d.mts.map +1 -1
  47. package/dist/number/enum/uint8.mjs.map +1 -1
  48. package/dist/number/num-arithmetic-types.d.mts +8 -9
  49. package/dist/number/num-arithmetic-types.d.mts.map +1 -1
  50. package/dist/number/refined-number-utils.d.mts +2 -2
  51. package/dist/number/refined-number-utils.d.mts.map +1 -1
  52. package/dist/number/refined-number-utils.mjs.map +1 -1
  53. package/dist/object/object.d.mts +12 -12
  54. package/dist/object/object.d.mts.map +1 -1
  55. package/dist/object/object.mjs +5 -1
  56. package/dist/object/object.mjs.map +1 -1
  57. package/dist/others/fast-deep-equal.d.mts.map +1 -1
  58. package/dist/others/fast-deep-equal.mjs +2 -2
  59. package/dist/others/fast-deep-equal.mjs.map +1 -1
  60. package/package.json +2 -2
  61. package/src/array/impl/array-utils-modification.test.mts +26 -5
  62. package/src/array/impl/array-utils-shape-invariants.test.mts +259 -17
  63. package/src/array/impl/array-utils-slicing.test.mts +11 -3
  64. package/src/array/impl/array-utils-transformation.mts +41 -18
  65. package/src/array/impl/array-utils-transformation.test.mts +10 -3
  66. package/src/array/impl/array-utils-validation.mts +54 -10
  67. package/src/array/impl/array-utils-validation.test.mts +153 -3
  68. package/src/collections/imap-mapped.mts +1 -1
  69. package/src/collections/imap-mapped.test.mts +2 -2
  70. package/src/collections/imap.mts +1 -1
  71. package/src/collections/iset-mapped.test.mts +1 -1
  72. package/src/functional/match.mts +5 -4
  73. package/src/functional/optional/impl/optional-map.mts +16 -1
  74. package/src/functional/optional.test.mts +48 -1
  75. package/src/functional/result/impl/result-map-err.mts +16 -1
  76. package/src/functional/result/impl/result-map.mts +16 -1
  77. package/src/functional/result.test.mts +83 -3
  78. package/src/guard/is-primitive.test.mts +1 -1
  79. package/src/guard/is-record.test.mts +4 -4
  80. package/src/guard/is-type.test.mts +2 -2
  81. package/src/number/branded-types/finite-number.test.mts +2 -2
  82. package/src/number/branded-types/non-negative-finite-number.test.mts +2 -2
  83. package/src/number/branded-types/non-positive-finite-number.mts +1 -1
  84. package/src/number/branded-types/non-positive-finite-number.test.mts +5 -9
  85. package/src/number/branded-types/non-zero-finite-number.test.mts +2 -2
  86. package/src/number/branded-types/positive-finite-number.test.mts +2 -2
  87. package/src/number/enum/int8.mts +2 -1
  88. package/src/number/enum/uint8.mts +6 -2
  89. package/src/number/num-arithmetic-types.mts +125 -24
  90. package/src/number/num.test.mts +9 -9
  91. package/src/number/refined-number-utils.mts +2 -1
  92. package/src/object/object.mts +29 -17
  93. package/src/object/object.test.mts +2 -1
  94. package/src/others/fast-deep-equal.mts +6 -4
@@ -14,8 +14,26 @@ import {
14
14
  asMaxLengthArray,
15
15
  asMinLengthArray,
16
16
  } from './array-utils-length-bounded-array-cast.mjs';
17
- import { toFilled, toRangeFilled } from './array-utils-modification.mjs';
18
- import { map, toReversed, toSorted } from './array-utils-transformation.mjs';
17
+ import {
18
+ toFilled,
19
+ toInserted,
20
+ toPushed,
21
+ toRangeFilled,
22
+ toRemoved,
23
+ toUnshifted,
24
+ } from './array-utils-modification.mjs';
25
+ import { skip, skipLast, take, takeLast } from './array-utils-slicing.mjs';
26
+ import {
27
+ concat,
28
+ filter,
29
+ filterNot,
30
+ map,
31
+ toReversed,
32
+ toSorted,
33
+ toSortedBy,
34
+ uniq,
35
+ uniqBy,
36
+ } from './array-utils-transformation.mjs';
19
37
 
20
38
  /**
21
39
  * Invariant-based coverage for the array -> array transforms.
@@ -31,12 +49,14 @@ import { map, toReversed, toSorted } from './array-utils-transformation.mjs';
31
49
  * 1. **Length** — the result reports the same `length` as the input.
32
50
  * 2. **Bounds** — a length brand on the input survives onto the result.
33
51
  * 3. **Generic context** — from a caller that is itself generic over the
34
- * array, the result is assignable to the caller's own homomorphic mapping.
52
+ * array, the result is assignable to the caller's own annotation.
35
53
  *
36
- * Property 3 is the one with no coverage before this file, and is what the
37
- * `Arr.map` regression fixed in this PR violated: a conditional return type a
38
- * generic `Ar` cannot decide stays deferred, and its brand-carrying branch is
39
- * then not assignable to the caller's mapping.
54
+ * Property 3 is the one that keeps regressing, and the one a per-combination
55
+ * table cannot express: a conditional a generic `Ar` cannot decide stays
56
+ * deferred, and a deferred conditional carrying a brand-intersection branch is
57
+ * not an array type at all — so the result has no `entries()`, is not
58
+ * assignable to the caller's annotation, and in the worst case the bound
59
+ * arithmetic behind the brand exceeds the instantiation limit.
40
60
  */
41
61
 
42
62
  // The sample inputs. Between them they cover the axes: empty / singleton /
@@ -71,15 +91,15 @@ const exactLength: FixedLengthArray<3, number> = asFixedLengthArray(
71
91
  * Property 3 — generic context.
72
92
  *
73
93
  * Each of these is an assertion by construction: the annotated return type is
74
- * the caller's own homomorphic mapping, so the declaration only compiles while
94
+ * what the caller is entitled to expect, so the declaration only compiles while
75
95
  * the transform reports something assignable to it. There is nothing to run.
76
96
  *
77
- * Only the transforms that satisfy this today are listed. `toSorted`,
78
- * `toSortedBy`, `toReversed`, `toUpdated`, `tail`, `butLast` and `zip` do not
79
- * yet — see the PR description for why each one resists the same fix. Add them
80
- * here as they are fixed; that is what makes this list the coverage record.
97
+ * This list is the coverage record. Every array -> array transform that
98
+ * resolves under a generic array parameter appears here; the ones that do not
99
+ * are listed at the bottom of this file, with the reason.
81
100
  */
82
101
 
102
+ // Element-replacing transforms report the caller's own homomorphic mapping.
83
103
  const mapInGenericContext = <const T extends readonly number[]>(
84
104
  xs: T,
85
105
  ): Readonly<{ [K in keyof T]: string }> => map(xs, String);
@@ -100,6 +120,85 @@ const toRangeFilledInGenericContext = <const T extends readonly number[]>(
100
120
  xs: T,
101
121
  ): Readonly<{ [K in keyof T]: number }> => toRangeFilled(xs, 0, [0, 1]);
102
122
 
123
+ // Reordering transforms keep the elements, so the caller is entitled to an
124
+ // array of them.
125
+ const toReversedInGenericContext = <const T extends readonly number[]>(
126
+ xs: T,
127
+ ): readonly T[number][] => toReversed(xs);
128
+
129
+ const toSortedInGenericContext = <const T extends readonly number[]>(
130
+ xs: T,
131
+ ): readonly T[number][] => toSorted(xs, (a, b) => a - b);
132
+
133
+ // Numbers sort without a comparator. This is the overload that makes it
134
+ // optional, which a generic caller could not reach at all while the parameter
135
+ // list was itself a conditional type.
136
+ const toSortedDefaultInGenericContext = <const T extends readonly number[]>(
137
+ xs: T,
138
+ ): readonly T[number][] => toSorted(xs);
139
+
140
+ const toSortedNonNumberInGenericContext = <const T extends readonly string[]>(
141
+ xs: T,
142
+ ): readonly T[number][] => toSorted(xs, (a, b) => a.localeCompare(b));
143
+
144
+ const toSortedByInGenericContext = <const T extends readonly number[]>(
145
+ xs: T,
146
+ ): readonly T[number][] => toSortedBy(xs, (n) => n);
147
+
148
+ // Shortening transforms lose the length but keep the element type.
149
+ const takeInGenericContext = <const T extends readonly number[]>(
150
+ xs: T,
151
+ ): readonly T[number][] => take(xs, 2);
152
+
153
+ const takeLastInGenericContext = <const T extends readonly number[]>(
154
+ xs: T,
155
+ ): readonly T[number][] => takeLast(xs, 2);
156
+
157
+ const skipInGenericContext = <const T extends readonly number[]>(
158
+ xs: T,
159
+ ): readonly T[number][] => skip(xs, 2);
160
+
161
+ const skipLastInGenericContext = <const T extends readonly number[]>(
162
+ xs: T,
163
+ ): readonly T[number][] => skipLast(xs, 2);
164
+
165
+ const toRemovedInGenericContext = <const T extends readonly number[]>(
166
+ xs: T,
167
+ ): readonly T[number][] => toRemoved(xs, 0);
168
+
169
+ const filterInGenericContext = <const T extends readonly number[]>(
170
+ xs: T,
171
+ ): readonly T[number][] => filter(xs, (n) => n > 0);
172
+
173
+ const filterNotInGenericContext = <const T extends readonly number[]>(
174
+ xs: T,
175
+ ): readonly T[number][] => filterNot(xs, (n) => n > 0);
176
+
177
+ const uniqInGenericContext = <const T extends readonly number[]>(
178
+ xs: T,
179
+ ): readonly T[number][] => uniq(xs);
180
+
181
+ const uniqByInGenericContext = <const T extends readonly number[]>(
182
+ xs: T,
183
+ ): readonly T[number][] => uniqBy(xs, (n) => n);
184
+
185
+ // Lengthening transforms widen the element type by whatever was added.
186
+ const toPushedInGenericContext = <const T extends readonly number[]>(
187
+ xs: T,
188
+ ): readonly (T[number] | 'x')[] => toPushed(xs, 'x');
189
+
190
+ const toUnshiftedInGenericContext = <const T extends readonly number[]>(
191
+ xs: T,
192
+ ): readonly (T[number] | 'x')[] => toUnshifted(xs, 'x');
193
+
194
+ const toInsertedInGenericContext = <const T extends readonly number[]>(
195
+ xs: T,
196
+ ): readonly (T[number] | 'x')[] => toInserted(xs, 0, 'x');
197
+
198
+ const concatInGenericContext = <const T extends readonly number[]>(
199
+ xs: T,
200
+ ): readonly T[number][] => concat(xs, xs);
201
+
103
202
  describe('Arr shape invariants', () => {
104
203
  // The type-level assertions below are checked by `tsc`; this pins the same
105
204
  // property at runtime, so a transform that silently drops or adds an element
@@ -107,19 +206,20 @@ describe('Arr shape invariants', () => {
107
206
  test('every shape-preserving transform keeps the length at runtime', () => {
108
207
  const input = [3, 1, 2] as const;
109
208
 
110
- assert.deepStrictEqual<FixedLengthTuple<5, 3>>(
209
+ assert.deepStrictEqual<FixedLengthTuple<6, 3>>(
111
210
  [
112
211
  map(input, String).length,
113
212
  toFilled(input, 0).length,
114
213
  toRangeFilled(input, 0, [1, 2]).length,
115
214
  toSorted(input).length,
215
+ toSortedBy(input, (n) => n).length,
116
216
  toReversed(input).length,
117
217
  ] as const,
118
- [3, 3, 3, 3, 3] as const,
218
+ [3, 3, 3, 3, 3, 3] as const,
119
219
  );
120
220
  });
121
221
 
122
- test('the generic-context callers agree with their annotations', () => {
222
+ test('the element-replacing callers agree with their annotations', () => {
123
223
  const input = [3, 1, 2] as const;
124
224
 
125
225
  assert.deepStrictEqual<FixedLengthTuple<3, string>>(
@@ -147,6 +247,105 @@ describe('Arr shape invariants', () => {
147
247
  [0, 1, 2],
148
248
  );
149
249
  });
250
+
251
+ test('the reordering callers agree with their annotations', () => {
252
+ const input = [3, 1, 2] as const;
253
+
254
+ assert.deepStrictEqual<readonly number[]>(
255
+ toReversedInGenericContext(input),
256
+ [2, 1, 3],
257
+ );
258
+
259
+ assert.deepStrictEqual<readonly number[]>(
260
+ toSortedInGenericContext(input),
261
+ [1, 2, 3],
262
+ );
263
+
264
+ assert.deepStrictEqual<readonly number[]>(
265
+ toSortedDefaultInGenericContext(input),
266
+ [1, 2, 3],
267
+ );
268
+
269
+ assert.deepStrictEqual<readonly string[]>(
270
+ toSortedNonNumberInGenericContext(['c', 'a', 'b'] as const),
271
+ ['a', 'b', 'c'],
272
+ );
273
+
274
+ assert.deepStrictEqual<readonly number[]>(
275
+ toSortedByInGenericContext(input),
276
+ [1, 2, 3],
277
+ );
278
+ });
279
+
280
+ test('the shortening callers agree with their annotations', () => {
281
+ const input = [3, 1, 2] as const;
282
+
283
+ assert.deepStrictEqual<readonly number[]>(
284
+ takeInGenericContext(input),
285
+ [3, 1],
286
+ );
287
+
288
+ assert.deepStrictEqual<readonly number[]>(
289
+ takeLastInGenericContext(input),
290
+ [1, 2],
291
+ );
292
+
293
+ assert.deepStrictEqual<readonly number[]>(skipInGenericContext(input), [2]);
294
+
295
+ assert.deepStrictEqual<readonly number[]>(
296
+ skipLastInGenericContext(input),
297
+ [3],
298
+ );
299
+
300
+ assert.deepStrictEqual<readonly number[]>(
301
+ toRemovedInGenericContext(input),
302
+ [1, 2],
303
+ );
304
+
305
+ assert.deepStrictEqual<readonly number[]>(
306
+ filterInGenericContext(input),
307
+ [3, 1, 2],
308
+ );
309
+
310
+ assert.deepStrictEqual<readonly number[]>(
311
+ filterNotInGenericContext(input),
312
+ [],
313
+ );
314
+
315
+ assert.deepStrictEqual<readonly number[]>(
316
+ uniqInGenericContext([1, 1, 2] as const),
317
+ [1, 2],
318
+ );
319
+
320
+ assert.deepStrictEqual<readonly number[]>(
321
+ uniqByInGenericContext([1, 1, 2] as const),
322
+ [1, 2],
323
+ );
324
+ });
325
+
326
+ test('the lengthening callers agree with their annotations', () => {
327
+ const input = [3, 1, 2] as const;
328
+
329
+ assert.deepStrictEqual<readonly (number | string)[]>(
330
+ toPushedInGenericContext(input),
331
+ [3, 1, 2, 'x'],
332
+ );
333
+
334
+ assert.deepStrictEqual<readonly (number | string)[]>(
335
+ toUnshiftedInGenericContext(input),
336
+ ['x', 3, 1, 2],
337
+ );
338
+
339
+ assert.deepStrictEqual<readonly (number | string)[]>(
340
+ toInsertedInGenericContext(input),
341
+ ['x', 3, 1, 2],
342
+ );
343
+
344
+ assert.deepStrictEqual<readonly number[]>(
345
+ concatInGenericContext(input),
346
+ [3, 1, 2, 3, 1, 2],
347
+ );
348
+ });
150
349
  });
151
350
 
152
351
  // `map` — property 1 (length) across every unbranded sample.
@@ -235,8 +434,7 @@ describe('Arr shape invariants', () => {
235
434
  expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
236
435
  }
237
436
 
238
- // `toSorted` — properties 1 and 2. Sorting neither adds nor drops elements, so
239
- // it is held to the same contract even though it is not fixed for property 3.
437
+ // `toSorted` — properties 1 and 2. Sorting neither adds nor drops elements.
240
438
  {
241
439
  const _a = toSorted(fixedTuple);
242
440
 
@@ -261,6 +459,27 @@ describe('Arr shape invariants', () => {
261
459
  expectType<MaxLengthOf<typeof _e>, MaxLengthOf<typeof bounded>>('=');
262
460
  }
263
461
 
462
+ // `toSortedBy` — properties 1 and 2.
463
+ {
464
+ const _a = toSortedBy(fixedTuple, (n) => n);
465
+
466
+ const _b = toSortedBy(plainArray, (n) => n);
467
+
468
+ const _c = toSortedBy(lowerBounded, (n) => n);
469
+
470
+ const _d = toSortedBy(bounded, (n) => n);
471
+
472
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
473
+
474
+ expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
475
+
476
+ expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
477
+
478
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof bounded>>('=');
479
+
480
+ expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof bounded>>('=');
481
+ }
482
+
264
483
  // `toReversed` — properties 1 and 2. Reversal permutes, so the length and the
265
484
  // bounds are exactly what must not move.
266
485
  {
@@ -282,3 +501,26 @@ describe('Arr shape invariants', () => {
282
501
 
283
502
  expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof bounded>>('=');
284
503
  }
504
+
505
+ /**
506
+ * The transforms that do **not** resolve under a generic array parameter, and
507
+ * therefore have no property-3 entry above:
508
+ *
509
+ * | transform | reports | symptom under a generic `Ar` |
510
+ * | :------------------ | :---------------------- | :--------------------------- |
511
+ * | `tail` / `butLast` | `ConstrainedList.Tail` | TS2590 |
512
+ * | `zip` | `ConstrainedList.Zip` | TS2589, and no `entries()` |
513
+ * | `set` / `toUpdated` | `ConstrainedList.SetAt` | TS2589 |
514
+ *
515
+ * The cause is the same in all three, and it is upstream. Their brand branch
516
+ * does arithmetic on the input's own bounds (`SubLength`, `SmallerLength`), and
517
+ * that arithmetic goes through `MakeTuple`. For a concrete branded array the
518
+ * bound is a single number and the expansion is cheap; for a type parameter it
519
+ * is the whole `SupportedLength` union, which `MakeTuple` distributes over.
520
+ *
521
+ * `map`, `toFilled` and `toRangeFilled` were fixable in 14.0.1 precisely
522
+ * because `ChangeArrayElement`'s brand branch does no such arithmetic. The
523
+ * `UnknownBrand`-keyed overload split that fixed them does not transfer: the
524
+ * constrained overload makes the checker attempt that arithmetic at every call
525
+ * site, generic or not.
526
+ */
@@ -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;