ts-data-forge 14.0.0 → 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 (100) hide show
  1. package/README.md +8 -7
  2. package/dist/array/impl/array-utils-modification.d.mts +18 -5
  3. package/dist/array/impl/array-utils-modification.d.mts.map +1 -1
  4. package/dist/array/impl/array-utils-modification.mjs.map +1 -1
  5. package/dist/array/impl/array-utils-slice-clamped.d.mts.map +1 -1
  6. package/dist/array/impl/array-utils-slice-clamped.mjs.map +1 -1
  7. package/dist/array/impl/array-utils-transformation.d.mts +12 -4
  8. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  9. package/dist/array/impl/array-utils-transformation.mjs +4 -5
  10. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  11. package/dist/array/impl/array-utils-validation.d.mts +8 -3
  12. package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
  13. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  14. package/dist/collections/imap-mapped.mjs +1 -1
  15. package/dist/collections/imap-mapped.mjs.map +1 -1
  16. package/dist/collections/imap.d.mts +1 -1
  17. package/dist/collections/imap.mjs +1 -1
  18. package/dist/collections/imap.mjs.map +1 -1
  19. package/dist/collections/iset-mapped.d.mts +2 -2
  20. package/dist/collections/iset-mapped.d.mts.map +1 -1
  21. package/dist/collections/iset-mapped.mjs.map +1 -1
  22. package/dist/collections/iset.d.mts +2 -2
  23. package/dist/collections/iset.d.mts.map +1 -1
  24. package/dist/collections/iset.mjs.map +1 -1
  25. package/dist/functional/match.d.mts.map +1 -1
  26. package/dist/functional/match.mjs.map +1 -1
  27. package/dist/number/branded-types/int.d.mts +1 -1
  28. package/dist/number/branded-types/int16.d.mts +1 -1
  29. package/dist/number/branded-types/int32.d.mts +1 -1
  30. package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
  31. package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
  32. package/dist/number/branded-types/non-positive-finite-number.mjs +1 -1
  33. package/dist/number/branded-types/non-positive-finite-number.mjs.map +1 -1
  34. package/dist/number/branded-types/positive-int.d.mts +1 -1
  35. package/dist/number/branded-types/positive-int16.d.mts +1 -1
  36. package/dist/number/branded-types/positive-int32.d.mts +1 -1
  37. package/dist/number/branded-types/positive-safe-int.d.mts +1 -1
  38. package/dist/number/branded-types/positive-uint16.d.mts +1 -1
  39. package/dist/number/branded-types/positive-uint32.d.mts +1 -1
  40. package/dist/number/branded-types/safe-int.d.mts +1 -1
  41. package/dist/number/branded-types/safe-uint.d.mts +1 -1
  42. package/dist/number/branded-types/uint.d.mts +1 -1
  43. package/dist/number/branded-types/uint16.d.mts +1 -1
  44. package/dist/number/branded-types/uint32.d.mts +1 -1
  45. package/dist/number/enum/int8.d.mts +2 -2
  46. package/dist/number/enum/int8.d.mts.map +1 -1
  47. package/dist/number/enum/int8.mjs.map +1 -1
  48. package/dist/number/enum/uint8.d.mts +2 -2
  49. package/dist/number/enum/uint8.d.mts.map +1 -1
  50. package/dist/number/enum/uint8.mjs.map +1 -1
  51. package/dist/number/num-arithmetic-types.d.mts +9 -10
  52. package/dist/number/num-arithmetic-types.d.mts.map +1 -1
  53. package/dist/number/refined-number-utils.d.mts +2 -2
  54. package/dist/number/refined-number-utils.d.mts.map +1 -1
  55. package/dist/number/refined-number-utils.mjs.map +1 -1
  56. package/dist/object/object.d.mts +12 -12
  57. package/dist/object/object.d.mts.map +1 -1
  58. package/dist/object/object.mjs +5 -1
  59. package/dist/object/object.mjs.map +1 -1
  60. package/dist/others/fast-deep-equal.d.mts.map +1 -1
  61. package/dist/others/fast-deep-equal.mjs +2 -2
  62. package/dist/others/fast-deep-equal.mjs.map +1 -1
  63. package/package.json +4 -5
  64. package/src/array/impl/array-utils-creation.test.mts +10 -8
  65. package/src/array/impl/array-utils-length-constraint-preservation.test.mts +2 -2
  66. package/src/array/impl/array-utils-modification.mts +41 -33
  67. package/src/array/impl/array-utils-modification.test.mts +77 -4
  68. package/src/array/impl/array-utils-shape-invariants.test.mts +526 -0
  69. package/src/array/impl/array-utils-slice-clamped.mts +2 -1
  70. package/src/array/impl/array-utils-slicing.test.mts +11 -3
  71. package/src/array/impl/array-utils-transformation.mts +70 -20
  72. package/src/array/impl/array-utils-transformation.test.mts +44 -10
  73. package/src/array/impl/array-utils-validation.mts +54 -10
  74. package/src/array/impl/array-utils-validation.test.mts +153 -3
  75. package/src/collections/imap-mapped.mts +1 -1
  76. package/src/collections/imap-mapped.test.mts +2 -2
  77. package/src/collections/imap.mts +2 -2
  78. package/src/collections/iset-mapped.mts +7 -3
  79. package/src/collections/iset-mapped.test.mts +1 -1
  80. package/src/collections/iset.mts +7 -3
  81. package/src/collections/iset.test.mts +9 -6
  82. package/src/functional/match.mts +5 -4
  83. package/src/functional/result.test.mts +5 -2
  84. package/src/guard/is-primitive.test.mts +1 -1
  85. package/src/guard/is-record.test.mts +4 -4
  86. package/src/guard/is-type.test.mts +2 -2
  87. package/src/number/branded-types/finite-number.test.mts +2 -2
  88. package/src/number/branded-types/non-negative-finite-number.test.mts +2 -2
  89. package/src/number/branded-types/non-positive-finite-number.mts +1 -1
  90. package/src/number/branded-types/non-positive-finite-number.test.mts +5 -9
  91. package/src/number/branded-types/non-zero-finite-number.test.mts +2 -2
  92. package/src/number/branded-types/positive-finite-number.test.mts +2 -2
  93. package/src/number/enum/int8.mts +2 -1
  94. package/src/number/enum/uint8.mts +6 -2
  95. package/src/number/num-arithmetic-types.mts +129 -24
  96. package/src/number/num.test.mts +11 -11
  97. package/src/number/refined-number-utils.mts +2 -1
  98. package/src/object/object.mts +29 -17
  99. package/src/object/object.test.mts +2 -1
  100. package/src/others/fast-deep-equal.mts +6 -4
@@ -0,0 +1,526 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type FixedLengthTuple,
5
+ type MaxLengthArray,
6
+ type MaxLengthOf,
7
+ type MinLengthArray,
8
+ type MinLengthOf,
9
+ } from 'ts-type-forge';
10
+ import { expectType } from '../../expect-type.mjs';
11
+ import {
12
+ asBoundedLengthArray,
13
+ asFixedLengthArray,
14
+ asMaxLengthArray,
15
+ asMinLengthArray,
16
+ } from './array-utils-length-bounded-array-cast.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';
37
+
38
+ /**
39
+ * Invariant-based coverage for the array -> array transforms.
40
+ *
41
+ * Rather than pinning an expected type for every (brand x shape x context)
42
+ * combination — which is a large table whose expected side is easy to derive
43
+ * from the implementation and therefore prove nothing — this asserts the small
44
+ * set of properties every shape-preserving transform must satisfy, across a
45
+ * fixed list of sample inputs that covers the axes.
46
+ *
47
+ * The properties are:
48
+ *
49
+ * 1. **Length** — the result reports the same `length` as the input.
50
+ * 2. **Bounds** — a length brand on the input survives onto the result.
51
+ * 3. **Generic context** — from a caller that is itself generic over the
52
+ * array, the result is assignable to the caller's own annotation.
53
+ *
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.
60
+ */
61
+
62
+ // The sample inputs. Between them they cover the axes: empty / singleton /
63
+ // fixed tuple / non-empty tuple / unbounded array on the shape axis, and
64
+ // unbranded / lower bound / upper bound / both / exact on the brand axis.
65
+ const emptyTuple: readonly [] = [] as const;
66
+
67
+ const singleton: readonly [1] = [1] as const;
68
+
69
+ const fixedTuple: readonly [1, 2, 3] = [1, 2, 3] as const;
70
+
71
+ const nonEmptyTuple: readonly [1, ...number[]] = [1, 2, 3] as const;
72
+
73
+ const plainArray: readonly number[] = [1, 2, 3] as const;
74
+
75
+ const lowerBounded: MinLengthArray<2, number> = asMinLengthArray(2, [1, 2, 3]);
76
+
77
+ const upperBounded: MaxLengthArray<5, number> = asMaxLengthArray(5, [1, 2, 3]);
78
+
79
+ const bounded: BoundedLengthArray<2, 5, number> = asBoundedLengthArray(
80
+ 2,
81
+ 5,
82
+ [1, 2, 3],
83
+ );
84
+
85
+ const exactLength: FixedLengthArray<3, number> = asFixedLengthArray(
86
+ 3,
87
+ [1, 2, 3],
88
+ );
89
+
90
+ /**
91
+ * Property 3 — generic context.
92
+ *
93
+ * Each of these is an assertion by construction: the annotated return type is
94
+ * what the caller is entitled to expect, so the declaration only compiles while
95
+ * the transform reports something assignable to it. There is nothing to run.
96
+ *
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.
100
+ */
101
+
102
+ // Element-replacing transforms report the caller's own homomorphic mapping.
103
+ const mapInGenericContext = <const T extends readonly number[]>(
104
+ xs: T,
105
+ ): Readonly<{ [K in keyof T]: string }> => map(xs, String);
106
+
107
+ const mapCurriedInGenericContext = <const T extends readonly number[]>(
108
+ xs: T,
109
+ ): Readonly<{ [K in keyof T]: string }> => map(String)(xs);
110
+
111
+ const toFilledInGenericContext = <const T extends readonly number[]>(
112
+ xs: T,
113
+ ): Readonly<{ [K in keyof T]: 0 }> => toFilled(xs, 0);
114
+
115
+ const toFilledCurriedInGenericContext = <const T extends readonly number[]>(
116
+ xs: T,
117
+ ): Readonly<{ [K in keyof T]: 0 }> => toFilled(0)(xs);
118
+
119
+ const toRangeFilledInGenericContext = <const T extends readonly number[]>(
120
+ xs: T,
121
+ ): Readonly<{ [K in keyof T]: number }> => toRangeFilled(xs, 0, [0, 1]);
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
+
202
+ describe('Arr shape invariants', () => {
203
+ // The type-level assertions below are checked by `tsc`; this pins the same
204
+ // property at runtime, so a transform that silently drops or adds an element
205
+ // fails here even where the type says otherwise.
206
+ test('every shape-preserving transform keeps the length at runtime', () => {
207
+ const input = [3, 1, 2] as const;
208
+
209
+ assert.deepStrictEqual<FixedLengthTuple<6, 3>>(
210
+ [
211
+ map(input, String).length,
212
+ toFilled(input, 0).length,
213
+ toRangeFilled(input, 0, [1, 2]).length,
214
+ toSorted(input).length,
215
+ toSortedBy(input, (n) => n).length,
216
+ toReversed(input).length,
217
+ ] as const,
218
+ [3, 3, 3, 3, 3, 3] as const,
219
+ );
220
+ });
221
+
222
+ test('the element-replacing callers agree with their annotations', () => {
223
+ const input = [3, 1, 2] as const;
224
+
225
+ assert.deepStrictEqual<FixedLengthTuple<3, string>>(
226
+ mapInGenericContext(input),
227
+ ['3', '1', '2'],
228
+ );
229
+
230
+ assert.deepStrictEqual<FixedLengthTuple<3, string>>(
231
+ mapCurriedInGenericContext(input),
232
+ ['3', '1', '2'],
233
+ );
234
+
235
+ assert.deepStrictEqual<FixedLengthTuple<3, 0>>(
236
+ toFilledInGenericContext(input),
237
+ [0, 0, 0],
238
+ );
239
+
240
+ assert.deepStrictEqual<FixedLengthTuple<3, 0>>(
241
+ toFilledCurriedInGenericContext(input),
242
+ [0, 0, 0],
243
+ );
244
+
245
+ assert.deepStrictEqual<FixedLengthTuple<3, number>>(
246
+ toRangeFilledInGenericContext(input),
247
+ [0, 1, 2],
248
+ );
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
+ });
349
+ });
350
+
351
+ // `map` — property 1 (length) across every unbranded sample.
352
+ {
353
+ const _a = map(emptyTuple, String);
354
+
355
+ const _b = map(singleton, String);
356
+
357
+ const _c = map(fixedTuple, String);
358
+
359
+ const _d = map(nonEmptyTuple, String);
360
+
361
+ const _e = map(plainArray, String);
362
+
363
+ expectType<(typeof _a)['length'], (typeof emptyTuple)['length']>('=');
364
+
365
+ expectType<(typeof _b)['length'], (typeof singleton)['length']>('=');
366
+
367
+ expectType<(typeof _c)['length'], (typeof fixedTuple)['length']>('=');
368
+
369
+ expectType<(typeof _d)['length'], (typeof nonEmptyTuple)['length']>('=');
370
+
371
+ expectType<(typeof _e)['length'], (typeof plainArray)['length']>('=');
372
+ }
373
+
374
+ // `map` — property 2 (bounds) across every branded sample.
375
+ {
376
+ const _a = map(lowerBounded, String);
377
+
378
+ const _b = map(upperBounded, String);
379
+
380
+ const _c = map(bounded, String);
381
+
382
+ const _d = map(exactLength, String);
383
+
384
+ expectType<MinLengthOf<typeof _a>, MinLengthOf<typeof lowerBounded>>('=');
385
+
386
+ expectType<MaxLengthOf<typeof _b>, MaxLengthOf<typeof upperBounded>>('=');
387
+
388
+ expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof bounded>>('=');
389
+
390
+ expectType<MaxLengthOf<typeof _c>, MaxLengthOf<typeof bounded>>('=');
391
+
392
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof exactLength>>('=');
393
+
394
+ expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof exactLength>>('=');
395
+ }
396
+
397
+ // `toFilled` — properties 1 and 2.
398
+ {
399
+ const _a = toFilled(fixedTuple, 0);
400
+
401
+ const _b = toFilled(nonEmptyTuple, 0);
402
+
403
+ const _c = toFilled(plainArray, 0);
404
+
405
+ const _d = toFilled(lowerBounded, 0);
406
+
407
+ const _e = toFilled(bounded, 0);
408
+
409
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
410
+
411
+ expectType<(typeof _b)['length'], (typeof nonEmptyTuple)['length']>('=');
412
+
413
+ expectType<(typeof _c)['length'], (typeof plainArray)['length']>('=');
414
+
415
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof lowerBounded>>('=');
416
+
417
+ expectType<MinLengthOf<typeof _e>, MinLengthOf<typeof bounded>>('=');
418
+
419
+ expectType<MaxLengthOf<typeof _e>, MaxLengthOf<typeof bounded>>('=');
420
+ }
421
+
422
+ // `toRangeFilled` — properties 1 and 2.
423
+ {
424
+ const _a = toRangeFilled(fixedTuple, 0, [1, 2]);
425
+
426
+ const _b = toRangeFilled(plainArray, 0, [0, 1]);
427
+
428
+ const _c = toRangeFilled(lowerBounded, 0, [0, 1]);
429
+
430
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
431
+
432
+ expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
433
+
434
+ expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
435
+ }
436
+
437
+ // `toSorted` — properties 1 and 2. Sorting neither adds nor drops elements.
438
+ {
439
+ const _a = toSorted(fixedTuple);
440
+
441
+ const _b = toSorted(nonEmptyTuple);
442
+
443
+ const _c = toSorted(plainArray);
444
+
445
+ const _d = toSorted(lowerBounded);
446
+
447
+ const _e = toSorted(bounded);
448
+
449
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
450
+
451
+ expectType<(typeof _b)['length'], (typeof nonEmptyTuple)['length']>('=');
452
+
453
+ expectType<(typeof _c)['length'], (typeof plainArray)['length']>('=');
454
+
455
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof lowerBounded>>('=');
456
+
457
+ expectType<MinLengthOf<typeof _e>, MinLengthOf<typeof bounded>>('=');
458
+
459
+ expectType<MaxLengthOf<typeof _e>, MaxLengthOf<typeof bounded>>('=');
460
+ }
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
+
483
+ // `toReversed` — properties 1 and 2. Reversal permutes, so the length and the
484
+ // bounds are exactly what must not move.
485
+ {
486
+ const _a = toReversed(fixedTuple);
487
+
488
+ const _b = toReversed(plainArray);
489
+
490
+ const _c = toReversed(lowerBounded);
491
+
492
+ const _d = toReversed(bounded);
493
+
494
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
495
+
496
+ expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
497
+
498
+ expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
499
+
500
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof bounded>>('=');
501
+
502
+ expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof bounded>>('=');
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,3 +1,4 @@
1
+ import { type FixedLengthTuple } from 'ts-type-forge';
1
2
  import { Num } from '../../number/index.mjs';
2
3
  import { type ArgArrayIndexWithNegative, type SizeType } from '../../types.mjs';
3
4
 
@@ -36,7 +37,7 @@ export function sliceClamped<E>(
36
37
  SizeType.ArgArrWithNegative,
37
38
  SizeType.ArgArrWithNegative,
38
39
  ]
39
- | readonly [SizeType.ArgArrWithNegative, SizeType.ArgArrWithNegative]
40
+ | FixedLengthTuple<2, SizeType.ArgArrWithNegative>
40
41
  ): readonly E[] | ((array: readonly E[]) => readonly E[]) {
41
42
  switch (args.length) {
42
43
  case 3:
@@ -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
+ });
@@ -12,6 +12,7 @@ import {
12
12
  type Primitive,
13
13
  type SafeUintWithSmallInt,
14
14
  type SupportedLength,
15
+ type UnknownBrand,
15
16
  type WithSmallInt,
16
17
  } from 'ts-type-forge';
17
18
  import { IMap } from '../../collections/index.mjs';
@@ -46,15 +47,41 @@ import { size } from './array-utils-size.mjs';
46
47
  * assert.deepStrictEqual(indexed, ['0:1', '1:2', '2:3']);
47
48
  * ```
48
49
  */
49
- export function map<const Ar extends readonly unknown[], B>(
50
+ export function map<const Ar extends readonly unknown[] & UnknownBrand, B>(
50
51
  array: Ar,
51
52
  mapFn: (a: Ar[number], index: ArrayIndex<Ar>) => B,
52
53
  ): ChangeArrayElement<Ar, B>;
53
54
 
55
+ // A plain array or tuple carries no length brand, so the homomorphic mapping
56
+ // is the whole answer. Stating it directly — rather than going through
57
+ // `ChangeArrayElement`, whose `HasLengthConstraint` test a *generic* `Ar`
58
+ // cannot decide — is what keeps `map` usable inside a function that is itself
59
+ // generic over the tuple: a deferred conditional is not assignable to the
60
+ // caller's own `{ [K in keyof Ar]: B }`, while this is.
61
+ export function map<const Ar extends readonly unknown[], B>(
62
+ array: Ar,
63
+ mapFn: (a: Ar[number], index: ArrayIndex<Ar>) => B,
64
+ ): Readonly<{ [K in keyof Ar]: B }>;
65
+
54
66
  // curried version
67
+ //
68
+ // The two cases have to be overloads *of the returned function*, not two
69
+ // overloads of `map` itself: both curried signatures take the same single
70
+ // argument, so overload resolution would always pick the first and the second
71
+ // would be dead.
72
+ //
73
+ // Spelled as an intersection of two function types rather than one object type
74
+ // carrying two call signatures — the `convert-to-readonly` codemod wraps an
75
+ // inline object type in `Readonly<…>`, and a mapped type drops call
76
+ // signatures, which would leave the curried form with none at all.
55
77
  export function map<A, B>(
56
78
  mapFn: (a: A, index: SizeType.Arr) => B,
57
- ): <const Ar extends readonly A[]>(array: Ar) => ChangeArrayElement<Ar, B>;
79
+ ): (<const Ar extends readonly A[] & UnknownBrand>(
80
+ array: Ar,
81
+ ) => ChangeArrayElement<Ar, B>) &
82
+ (<const Ar extends readonly A[]>(
83
+ array: Ar,
84
+ ) => Readonly<{ [K in keyof Ar]: B }>);
58
85
 
59
86
  export function map<A, B>(
60
87
  ...args:
@@ -218,28 +245,51 @@ export const toReversed = <const Ar extends readonly unknown[]>(
218
245
  * assert.deepStrictEqual(alphabetical, expectedWords);
219
246
  * ```
220
247
  */
221
- export const toSorted = <const Ar extends readonly unknown[]>(
222
- ...[array, comparator]: Ar extends readonly number[]
223
- ? readonly [
224
- array: Ar,
225
- // If the array elements are mapped to numbers, comparator is optional.
226
- comparator?: (x: Ar[number], y: Ar[number]) => number,
227
- ]
228
- : readonly [array: Ar, comparator: (x: Ar[number], y: Ar[number]) => number]
229
- ): HasLengthConstraint<Ar> extends true
230
- ? ChangeArrayElement<Ar, Ar[number]>
231
- : IsFixedLengthList<Ar> extends true
232
- ? FixedLengthTuple<Ar['length'], Ar[number]>
233
- : Ar extends NonEmptyTuple<unknown>
234
- ? NonEmptyArray<Ar[number]>
235
- : readonly Ar[number][] =>
236
- // eslint-disable-next-line total-functions/no-unsafe-type-assertion
237
- 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(
238
277
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
239
278
  (comparator as ((x: unknown, y: unknown) => number) | undefined) ??
240
279
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
241
280
  ((x, y) => (x as number) - (y as number)),
242
- ) 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][];
243
293
 
244
294
  /**
245
295
  * Sorts an array by a mapped value.