ts-data-forge 13.0.0 → 14.0.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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/array/impl/array-utils-creation.d.mts +2 -2
  3. package/dist/array/impl/array-utils-creation.d.mts.map +1 -1
  4. package/dist/array/impl/array-utils-creation.mjs.map +1 -1
  5. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts +56 -29
  6. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts.map +1 -1
  7. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs +23 -15
  8. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs.map +1 -1
  9. package/dist/array/impl/array-utils-length-bounded-array-guard.d.mts +126 -28
  10. package/dist/array/impl/array-utils-length-bounded-array-guard.d.mts.map +1 -1
  11. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs +39 -5
  12. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs.map +1 -1
  13. package/dist/array/impl/array-utils-length-bounded-tuple-cast.d.mts +167 -0
  14. package/dist/array/impl/array-utils-length-bounded-tuple-cast.d.mts.map +1 -0
  15. package/dist/array/impl/array-utils-length-bounded-tuple-cast.mjs +87 -0
  16. package/dist/array/impl/array-utils-length-bounded-tuple-cast.mjs.map +1 -0
  17. package/dist/array/impl/array-utils-length-bounded-tuple-guard.d.mts +135 -0
  18. package/dist/array/impl/array-utils-length-bounded-tuple-guard.d.mts.map +1 -0
  19. package/dist/array/impl/array-utils-length-bounded-tuple-guard.mjs +41 -0
  20. package/dist/array/impl/array-utils-length-bounded-tuple-guard.mjs.map +1 -0
  21. package/dist/array/impl/array-utils-modification.d.mts +32 -18
  22. package/dist/array/impl/array-utils-modification.d.mts.map +1 -1
  23. package/dist/array/impl/array-utils-modification.mjs.map +1 -1
  24. package/dist/array/impl/array-utils-reducing-value.d.mts +3 -3
  25. package/dist/array/impl/array-utils-reducing-value.d.mts.map +1 -1
  26. package/dist/array/impl/array-utils-reducing-value.mjs +1 -1
  27. package/dist/array/impl/array-utils-reducing-value.mjs.map +1 -1
  28. package/dist/array/impl/array-utils-slicing.d.mts +11 -11
  29. package/dist/array/impl/array-utils-slicing.d.mts.map +1 -1
  30. package/dist/array/impl/array-utils-slicing.mjs +1 -1
  31. package/dist/array/impl/array-utils-slicing.mjs.map +1 -1
  32. package/dist/array/impl/array-utils-transformation.d.mts +35 -23
  33. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  34. package/dist/array/impl/array-utils-transformation.mjs +2 -2
  35. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  36. package/dist/array/impl/array-utils-validation.d.mts +2 -124
  37. package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
  38. package/dist/array/impl/array-utils-validation.mjs +1 -7
  39. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  40. package/dist/array/impl/index.d.mts +2 -0
  41. package/dist/array/impl/index.d.mts.map +1 -1
  42. package/dist/array/impl/index.mjs +5 -3
  43. package/dist/array/impl/index.mjs.map +1 -1
  44. package/dist/object/object.mjs +1 -1
  45. package/dist/string/str.d.mts +44 -44
  46. package/dist/string/str.d.mts.map +1 -1
  47. package/dist/string/str.mjs +17 -17
  48. package/dist/string/str.mjs.map +1 -1
  49. package/package.json +2 -2
  50. package/src/array/impl/array-utils-creation.mts +7 -3
  51. package/src/array/impl/array-utils-length-bounded-array-cast.mts +88 -50
  52. package/src/array/impl/array-utils-length-bounded-array-cast.test.mts +40 -13
  53. package/src/array/impl/array-utils-length-bounded-array-guard.mts +248 -39
  54. package/src/array/impl/array-utils-length-bounded-array-guard.test.mts +135 -24
  55. package/src/array/impl/array-utils-length-bounded-tuple-cast.mts +364 -0
  56. package/src/array/impl/array-utils-length-bounded-tuple-cast.test.mts +207 -0
  57. package/src/array/impl/array-utils-length-bounded-tuple-guard.mts +266 -0
  58. package/src/array/impl/array-utils-length-bounded-tuple-guard.test.mts +441 -0
  59. package/src/array/impl/array-utils-length-constraint-preservation.test.mts +287 -0
  60. package/src/array/impl/array-utils-modification.mts +72 -57
  61. package/src/array/impl/array-utils-modification.test.mts +93 -6
  62. package/src/array/impl/array-utils-reducing-value.mts +5 -5
  63. package/src/array/impl/array-utils-slicing.mts +65 -43
  64. package/src/array/impl/array-utils-slicing.test.mts +55 -0
  65. package/src/array/impl/array-utils-transformation.mts +66 -39
  66. package/src/array/impl/array-utils-transformation.test.mts +62 -16
  67. package/src/array/impl/array-utils-validation.mts +2 -162
  68. package/src/array/impl/array-utils-validation.test.mts +0 -371
  69. package/src/array/impl/index.mts +2 -0
  70. package/src/collections/queue.test.mts +1 -1
  71. package/src/string/str.mts +69 -69
  72. package/src/string/str.test.mts +40 -40
@@ -1,4 +1,6 @@
1
1
  import {
2
+ type ConstrainedList,
3
+ type HasLengthConstraint,
2
4
  type List,
3
5
  type NonEmptyArray,
4
6
  type NonEmptyTuple,
@@ -6,9 +8,9 @@ import {
6
8
  } from 'ts-type-forge';
7
9
  import { Uint32 } from '../../number/index.mjs';
8
10
  import { type SizeType } from '../../types.mjs';
11
+ import { isEmpty } from './array-utils-length-bounded-array-guard.mjs';
9
12
  import { size } from './array-utils-size.mjs';
10
13
  import { sliceClamped } from './array-utils-slice-clamped.mjs';
11
- import { isEmpty } from './array-utils-validation.mjs';
12
14
 
13
15
  /**
14
16
  * Returns all elements of an array except the first one.
@@ -41,9 +43,9 @@ import { isEmpty } from './array-utils-validation.mjs';
41
43
  */
42
44
  export const tail = <const Ar extends readonly unknown[]>(
43
45
  array: Ar,
44
- ): List.Tail<Ar> =>
46
+ ): ConstrainedList.Tail<Ar> =>
45
47
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
46
- array.slice(1) as unknown as List.Tail<Ar>;
48
+ array.slice(1) as unknown as ConstrainedList.Tail<Ar>;
47
49
 
48
50
  /**
49
51
  * Returns all elements of an array except the last one.
@@ -62,9 +64,11 @@ export const tail = <const Ar extends readonly unknown[]>(
62
64
  */
63
65
  export const butLast = <const Ar extends readonly unknown[]>(
64
66
  array: Ar,
65
- ): List.ButLast<Ar> =>
67
+ ): ConstrainedList.ButLast<Ar> =>
66
68
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
67
- (isEmpty(array) ? [] : array.slice(0, -1)) as unknown as List.ButLast<Ar>;
69
+ (isEmpty(array)
70
+ ? []
71
+ : array.slice(0, -1)) as unknown as ConstrainedList.ButLast<Ar>;
68
72
 
69
73
  /**
70
74
  * Takes the first N elements from an array.
@@ -89,25 +93,29 @@ export function take<
89
93
  >(
90
94
  array: Ar,
91
95
  num: N,
92
- ): N extends StructuralPrefixLength
93
- ? List.Take<N, Ar>
94
- : N extends SizeType.ArgArrPositive
95
- ? Ar extends NonEmptyTuple<unknown>
96
- ? NonEmptyArray<Ar[number]>
97
- : readonly Ar[number][]
98
- : readonly Ar[number][];
96
+ ): HasLengthConstraint<Ar> extends true
97
+ ? readonly Ar[number][]
98
+ : N extends StructuralPrefixLength
99
+ ? List.Take<N, Ar>
100
+ : N extends SizeType.ArgArrPositive
101
+ ? Ar extends NonEmptyTuple<unknown>
102
+ ? NonEmptyArray<Ar[number]>
103
+ : readonly Ar[number][]
104
+ : readonly Ar[number][];
99
105
 
100
106
  export function take<N extends SizeType.ArgArr>(
101
107
  num: N,
102
108
  ): <const Ar extends readonly unknown[]>(
103
109
  array: Ar,
104
- ) => N extends StructuralPrefixLength
105
- ? List.Take<N, Ar>
106
- : N extends SizeType.ArgArrPositive
107
- ? Ar extends NonEmptyTuple<unknown>
108
- ? NonEmptyArray<Ar[number]>
109
- : readonly Ar[number][]
110
- : readonly Ar[number][];
110
+ ) => HasLengthConstraint<Ar> extends true
111
+ ? readonly Ar[number][]
112
+ : N extends StructuralPrefixLength
113
+ ? List.Take<N, Ar>
114
+ : N extends SizeType.ArgArrPositive
115
+ ? Ar extends NonEmptyTuple<unknown>
116
+ ? NonEmptyArray<Ar[number]>
117
+ : readonly Ar[number][]
118
+ : readonly Ar[number][];
111
119
 
112
120
  export function take<E>(
113
121
  ...args:
@@ -151,25 +159,29 @@ export function takeLast<
151
159
  >(
152
160
  array: Ar,
153
161
  num: N,
154
- ): N extends StructuralPrefixLength
155
- ? List.TakeLast<N, Ar>
156
- : N extends SizeType.ArgArrPositive
157
- ? Ar extends NonEmptyTuple<unknown>
158
- ? NonEmptyArray<Ar[number]>
159
- : readonly Ar[number][]
160
- : readonly Ar[number][];
162
+ ): HasLengthConstraint<Ar> extends true
163
+ ? readonly Ar[number][]
164
+ : N extends StructuralPrefixLength
165
+ ? List.TakeLast<N, Ar>
166
+ : N extends SizeType.ArgArrPositive
167
+ ? Ar extends NonEmptyTuple<unknown>
168
+ ? NonEmptyArray<Ar[number]>
169
+ : readonly Ar[number][]
170
+ : readonly Ar[number][];
161
171
 
162
172
  export function takeLast<N extends SizeType.ArgArr>(
163
173
  num: N,
164
174
  ): <const Ar extends readonly unknown[]>(
165
175
  array: Ar,
166
- ) => N extends StructuralPrefixLength
167
- ? List.TakeLast<N, Ar>
168
- : N extends SizeType.ArgArrPositive
169
- ? Ar extends NonEmptyTuple<unknown>
170
- ? NonEmptyArray<Ar[number]>
171
- : readonly Ar[number][]
172
- : readonly Ar[number][];
176
+ ) => HasLengthConstraint<Ar> extends true
177
+ ? readonly Ar[number][]
178
+ : N extends StructuralPrefixLength
179
+ ? List.TakeLast<N, Ar>
180
+ : N extends SizeType.ArgArrPositive
181
+ ? Ar extends NonEmptyTuple<unknown>
182
+ ? NonEmptyArray<Ar[number]>
183
+ : readonly Ar[number][]
184
+ : readonly Ar[number][];
173
185
 
174
186
  export function takeLast<E>(
175
187
  ...args:
@@ -214,16 +226,22 @@ export function skip<
214
226
  >(
215
227
  array: Ar,
216
228
  num: N,
217
- ): N extends StructuralPrefixLength ? List.Skip<N, Ar> : readonly Ar[number][];
229
+ ): HasLengthConstraint<Ar> extends true
230
+ ? readonly Ar[number][]
231
+ : N extends StructuralPrefixLength
232
+ ? List.Skip<N, Ar>
233
+ : readonly Ar[number][];
218
234
 
219
235
  // curried version
220
236
  export function skip<N extends SizeType.ArgArr>(
221
237
  num: N,
222
238
  ): <const Ar extends readonly unknown[]>(
223
239
  array: Ar,
224
- ) => N extends StructuralPrefixLength
225
- ? List.Skip<N, Ar>
226
- : readonly Ar[number][];
240
+ ) => HasLengthConstraint<Ar> extends true
241
+ ? readonly Ar[number][]
242
+ : N extends StructuralPrefixLength
243
+ ? List.Skip<N, Ar>
244
+ : readonly Ar[number][];
227
245
 
228
246
  export function skip<E>(
229
247
  ...args: readonly [readonly E[], SizeType.ArgArr] | readonly [SizeType.ArgArr]
@@ -265,18 +283,22 @@ export function skipLast<
265
283
  >(
266
284
  array: Ar,
267
285
  num: N,
268
- ): N extends StructuralPrefixLength
269
- ? List.SkipLast<N, Ar>
270
- : readonly Ar[number][];
286
+ ): HasLengthConstraint<Ar> extends true
287
+ ? readonly Ar[number][]
288
+ : N extends StructuralPrefixLength
289
+ ? List.SkipLast<N, Ar>
290
+ : readonly Ar[number][];
271
291
 
272
292
  // curried version
273
293
  export function skipLast<N extends SizeType.ArgArr>(
274
294
  num: N,
275
295
  ): <const Ar extends readonly unknown[]>(
276
296
  array: Ar,
277
- ) => N extends StructuralPrefixLength
278
- ? List.SkipLast<N, Ar>
279
- : readonly Ar[number][];
297
+ ) => HasLengthConstraint<Ar> extends true
298
+ ? readonly Ar[number][]
299
+ : N extends StructuralPrefixLength
300
+ ? List.SkipLast<N, Ar>
301
+ : readonly Ar[number][];
280
302
 
281
303
  export function skipLast<E>(
282
304
  ...args:
@@ -1,3 +1,4 @@
1
+ import { type MinLengthArray } from 'ts-type-forge';
1
2
  import { expectType } from '../../expect-type.mjs';
2
3
  import {
3
4
  butLast,
@@ -219,3 +220,57 @@ describe('Arr slicing', () => {
219
220
  });
220
221
  });
221
222
  });
223
+
224
+ /* Length-constrained inputs. `List.*` reports what it can see from a
225
+ fixed-length tuple, and a branded array's `length` is `number`, so it handed
226
+ 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 = (
230
+ brandedFive: MinLengthArray<5, string>,
231
+ brandedAndTuple: MinLengthArray<3, number> & readonly [1, 2, 3, 4, 5],
232
+ ): void => {
233
+ // The bound is recomputed rather than passed through.
234
+ const _tailed = tail(brandedFive);
235
+
236
+ expectType<typeof _tailed, MinLengthArray<4, string>>('~=');
237
+
238
+ const _butLasted = butLast(brandedFive);
239
+
240
+ expectType<typeof _butLasted, MinLengthArray<4, string>>('~=');
241
+
242
+ /* A brand intersected with an exact tuple keeps the positions too, so the
243
+ result is exact on both counts. */
244
+ const _tailedBoth = tail(brandedAndTuple);
245
+
246
+ expectType<(typeof _tailedBoth)[0], 2>('=');
247
+
248
+ expectType<(typeof _tailedBoth)['length'], 4>('=');
249
+
250
+ /* `take` / `takeLast` / `skip` / `skipLast` stay on the `List.*` path and give
251
+ a branded input the sound unconstrained answer instead of its stale brand.
252
+ They cannot use `ConstrainedList.Take` and friends: those rebuild the bounds
253
+ from `N` and cannot be instantiated against a still-generic `Ar` at all —
254
+ even with a literal `N` the deferred result is a union too complex to
255
+ represent. */
256
+ const _taken = take(brandedFive, 2);
257
+
258
+ expectType<typeof _taken, readonly string[]>('=');
259
+
260
+ const _skipped = skip(brandedFive, 2);
261
+
262
+ expectType<typeof _skipped, readonly string[]>('=');
263
+
264
+ // Plain tuples keep mapping exactly as before.
265
+ const _tailedTuple = tail([1, 2, 3] as const);
266
+
267
+ expectType<typeof _tailedTuple, readonly [2, 3]>('=');
268
+
269
+ const _butLastedTuple = butLast([1, 2, 3] as const);
270
+
271
+ expectType<typeof _butLastedTuple, readonly [1, 2]>('=');
272
+
273
+ const _takenTuple = take([1, 2, 3] as const, 2);
274
+
275
+ expectType<typeof _takenTuple, readonly [1, 2]>('=');
276
+ };
@@ -1,13 +1,17 @@
1
1
  import {
2
+ type BoundedLengthArray,
3
+ type ChangeArrayElement,
4
+ type ConstrainedList,
2
5
  type FixedLengthTuple,
6
+ type HasLengthConstraint,
3
7
  type IsFixedLengthList,
4
- type List,
5
8
  type NonEmptyArray,
6
9
  type NonEmptyTuple,
7
10
  type PositiveInt,
8
11
  type PositiveUint32,
9
12
  type Primitive,
10
13
  type SafeUintWithSmallInt,
14
+ type SupportedLength,
11
15
  type WithSmallInt,
12
16
  } from 'ts-type-forge';
13
17
  import { IMap } from '../../collections/index.mjs';
@@ -20,8 +24,8 @@ import {
20
24
  type SizeType,
21
25
  } from '../../types.mjs';
22
26
  import { newArray, seq } from './array-utils-creation.mjs';
27
+ import { isNonEmpty } from './array-utils-length-bounded-array-guard.mjs';
23
28
  import { size } from './array-utils-size.mjs';
24
- import { isNonEmpty } from './array-utils-validation.mjs';
25
29
 
26
30
  /**
27
31
  * Creates a new array by transforming each element with a mapping function.
@@ -45,14 +49,12 @@ import { isNonEmpty } from './array-utils-validation.mjs';
45
49
  export function map<const Ar extends readonly unknown[], B>(
46
50
  array: Ar,
47
51
  mapFn: (a: Ar[number], index: ArrayIndex<Ar>) => B,
48
- ): Readonly<{ [K in keyof Ar]: B }>;
52
+ ): ChangeArrayElement<Ar, B>;
49
53
 
50
54
  // curried version
51
55
  export function map<A, B>(
52
56
  mapFn: (a: A, index: SizeType.Arr) => B,
53
- ): <const Ar extends readonly A[]>(
54
- array: Ar,
55
- ) => Readonly<{ [K in keyof Ar]: B }>;
57
+ ): <const Ar extends readonly A[]>(array: Ar) => ChangeArrayElement<Ar, B>;
56
58
 
57
59
  export function map<A, B>(
58
60
  ...args:
@@ -187,7 +189,7 @@ const scanImpl = <E, S>(
187
189
  */
188
190
  export const toReversed = <const Ar extends readonly unknown[]>(
189
191
  array: Ar,
190
- ): List.Reverse<Ar> =>
192
+ ): ConstrainedList.Reverse<Ar> =>
191
193
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
192
194
  array.toReversed() as never;
193
195
 
@@ -224,11 +226,13 @@ export const toSorted = <const Ar extends readonly unknown[]>(
224
226
  comparator?: (x: Ar[number], y: Ar[number]) => number,
225
227
  ]
226
228
  : readonly [array: Ar, comparator: (x: Ar[number], y: Ar[number]) => number]
227
- ): IsFixedLengthList<Ar> extends true
228
- ? FixedLengthTuple<Ar['length'], Ar[number]>
229
- : Ar extends NonEmptyTuple<unknown>
230
- ? NonEmptyArray<Ar[number]>
231
- : readonly Ar[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][] =>
232
236
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
233
237
  array.toSorted(
234
238
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
@@ -282,21 +286,25 @@ export function toSortedBy<const Ar extends readonly unknown[]>(
282
286
  comparatorValueMapper: (value: Ar[number]) => number,
283
287
  // If the array elements are mapped to numbers, comparator is optional.
284
288
  comparator?: (x: number, y: number) => number,
285
- ): IsFixedLengthList<Ar> extends true
286
- ? FixedLengthTuple<Ar['length'], Ar[number]>
287
- : Ar extends NonEmptyTuple<unknown>
288
- ? NonEmptyArray<Ar[number]>
289
- : readonly Ar[number][];
289
+ ): HasLengthConstraint<Ar> extends true
290
+ ? ChangeArrayElement<Ar, Ar[number]>
291
+ : IsFixedLengthList<Ar> extends true
292
+ ? FixedLengthTuple<Ar['length'], Ar[number]>
293
+ : Ar extends NonEmptyTuple<unknown>
294
+ ? NonEmptyArray<Ar[number]>
295
+ : readonly Ar[number][];
290
296
 
291
297
  export function toSortedBy<const Ar extends readonly unknown[], const V>(
292
298
  array: Ar,
293
299
  comparatorValueMapper: (value: Ar[number]) => V,
294
300
  comparator: (x: V, y: V) => number,
295
- ): IsFixedLengthList<Ar> extends true
296
- ? FixedLengthTuple<Ar['length'], Ar[number]>
297
- : Ar extends NonEmptyTuple<unknown>
298
- ? NonEmptyArray<Ar[number]>
299
- : readonly Ar[number][];
301
+ ): HasLengthConstraint<Ar> extends true
302
+ ? ChangeArrayElement<Ar, Ar[number]>
303
+ : IsFixedLengthList<Ar> extends true
304
+ ? FixedLengthTuple<Ar['length'], Ar[number]>
305
+ : Ar extends NonEmptyTuple<unknown>
306
+ ? NonEmptyArray<Ar[number]>
307
+ : readonly Ar[number][];
300
308
 
301
309
  export function toSortedBy<E, const V>(
302
310
  array: readonly E[],
@@ -627,18 +635,24 @@ export function flatMap<A, B>(
627
635
  *
628
636
  * const triples = Arr.partition(3)(values);
629
637
  *
638
+ * // Every chunk is branded `BoundedLengthArray<1, N, …>` — `partition` never
639
+ * // emits an empty chunk — so the expected values are annotated with the
640
+ * // unbranded type.
630
641
  * const expectedPairs = [[1, 2], [3, 4], [5]] as const;
631
642
  *
632
- * assert.deepStrictEqual(pairs, expectedPairs);
643
+ * assert.deepStrictEqual<readonly (readonly number[])[]>(
644
+ * pairs,
645
+ * expectedPairs,
646
+ * );
633
647
  *
634
- * assert.deepStrictEqual(triples, [
648
+ * assert.deepStrictEqual<readonly (readonly number[])[]>(triples, [
635
649
  * [1, 2, 3],
636
650
  * [4, 5],
637
651
  * ]);
638
652
  *
639
653
  * const pairs2 = Arr.chunk([1, 2, 3, 4, 5, 6], 2);
640
654
  *
641
- * assert.deepStrictEqual(pairs2, [
655
+ * assert.deepStrictEqual<readonly (readonly number[])[]>(pairs2, [
642
656
  * [1, 2],
643
657
  * [3, 4],
644
658
  * [5, 6],
@@ -646,17 +660,17 @@ export function flatMap<A, B>(
646
660
  * ```
647
661
  */
648
662
  export function partition<
649
- N extends WithSmallInt<PositiveInt & SizeType.Arr>,
650
663
  E,
651
- >(array: readonly E[], chunkSize: N): readonly (readonly E[])[];
664
+ N extends WithSmallInt<PositiveInt & SizeType.Arr>,
665
+ >(array: readonly E[], chunkSize: N): readonly PartitionChunk<N, E>[];
652
666
 
653
667
  export function partition<N extends WithSmallInt<PositiveInt & SizeType.Arr>>(
654
668
  chunkSize: N,
655
- ): <E>(array: readonly E[]) => readonly (readonly E[])[];
669
+ ): <E>(array: readonly E[]) => readonly PartitionChunk<N, E>[];
656
670
 
657
671
  export function partition<
658
- N extends WithSmallInt<PositiveInt & SizeType.Arr>,
659
672
  E,
673
+ N extends WithSmallInt<PositiveInt & SizeType.Arr>,
660
674
  >(
661
675
  ...args:
662
676
  readonly [array: readonly E[], chunkSize: N] | readonly [chunkSize: N]
@@ -672,7 +686,20 @@ export function partition<
672
686
  }
673
687
  }
674
688
 
675
- const partitionImpl = <N extends WithSmallInt<PositiveInt & SizeType.Arr>, E>(
689
+ /**
690
+ * Element type of {@link partition}'s result: a chunk holds between 1 and `N`
691
+ * elements. The lower bound is what makes this worth spelling out — `partition`
692
+ * never emits an empty chunk, so indexed access into a chunk is not
693
+ * `undefined` at position `0`, unlike with a plain `readonly E[]`.
694
+ *
695
+ * Falls back to {@link NonEmptyArray} when `N` is not a bound the brand can
696
+ * express, which keeps that lower bound in every case.
697
+ */
698
+ type PartitionChunk<N, E> = N extends SupportedLength
699
+ ? BoundedLengthArray<1, N, E>
700
+ : NonEmptyArray<E>;
701
+
702
+ const partitionImpl = <E, N extends WithSmallInt<PositiveInt & SizeType.Arr>>(
676
703
  array: readonly E[],
677
704
  chunkSize: N,
678
705
  ): readonly (readonly E[])[] =>
@@ -683,6 +710,13 @@ const partitionImpl = <N extends WithSmallInt<PositiveInt & SizeType.Arr>, E>(
683
710
  array.slice(chunkSize * i, chunkSize * (i + 1)),
684
711
  );
685
712
 
713
+ /**
714
+ * Alias for `partition`.
715
+ *
716
+ * @see {@link partition}
717
+ */
718
+ export const chunk = partition;
719
+
686
720
  /**
687
721
  * Concatenates two arrays.
688
722
  *
@@ -853,20 +887,13 @@ export const zip = <
853
887
  >(
854
888
  array1: Ar1,
855
889
  array2: Ar2,
856
- ): List.Zip<Ar1, Ar2> =>
890
+ ): ConstrainedList.Zip<Ar1, Ar2> =>
857
891
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
858
892
  seq(Uint32.min(size(array1), size(array2))).map((i: Uint32) =>
859
893
  // Non-null assertion is safe here because `i` is always within bounds of both arrays up to the length of the shorter one.
860
894
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
861
895
  tp(array1[i]!, array2[i]!),
862
- ) as unknown as List.Zip<Ar1, Ar2>;
863
-
864
- /**
865
- * Alias for `partition`.
866
- *
867
- * @see {@link partition}
868
- */
869
- export const chunk = partition;
896
+ ) as unknown as ConstrainedList.Zip<Ar1, Ar2>;
870
897
 
871
898
  /**
872
899
  * Computes the cartesian product of arrays.
@@ -1,4 +1,9 @@
1
- import { type FixedLengthTuple, type NonEmptyArray } from 'ts-type-forge';
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthTuple,
4
+ type MinLengthArray,
5
+ type NonEmptyArray,
6
+ } from 'ts-type-forge';
2
7
  import { IMap } from '../../collections/index.mjs';
3
8
  import { expectType } from '../../expect-type.mjs';
4
9
  import { Optional } from '../../functional/index.mjs';
@@ -644,13 +649,15 @@ describe('Arr transformations', () => {
644
649
 
645
650
  expectType<
646
651
  typeof result,
647
- readonly (readonly (
652
+ readonly BoundedLengthArray<
653
+ 1,
654
+ 4,
648
655
  1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
649
- )[])[]
650
- >('=');
656
+ >[]
657
+ >('~=');
651
658
 
652
659
  test('case 1', () => {
653
- assert.deepStrictEqual(result, [
660
+ assert.deepStrictEqual<readonly (readonly number[])[]>(result, [
654
661
  [1, 2, 3, 4],
655
662
  [5, 6, 7, 8],
656
663
  [9, 10, 11, 12],
@@ -663,13 +670,15 @@ describe('Arr transformations', () => {
663
670
 
664
671
  expectType<
665
672
  typeof result,
666
- readonly (readonly (
673
+ readonly BoundedLengthArray<
674
+ 1,
675
+ 3,
667
676
  1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
668
- )[])[]
669
- >('=');
677
+ >[]
678
+ >('~=');
670
679
 
671
680
  test('case 2', () => {
672
- assert.deepStrictEqual(result, [
681
+ assert.deepStrictEqual<readonly (readonly number[])[]>(result, [
673
682
  [1, 2, 3],
674
683
  [4, 5, 6],
675
684
  [7, 8, 9],
@@ -683,13 +692,15 @@ describe('Arr transformations', () => {
683
692
 
684
693
  expectType<
685
694
  typeof result,
686
- readonly (readonly (
695
+ readonly BoundedLengthArray<
696
+ 1,
697
+ 5,
687
698
  1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
688
- )[])[]
689
- >('=');
699
+ >[]
700
+ >('~=');
690
701
 
691
702
  test('case 3', () => {
692
- assert.deepStrictEqual(result, [
703
+ assert.deepStrictEqual<readonly (readonly number[])[]>(result, [
693
704
  [1, 2, 3, 4, 5],
694
705
  [6, 7, 8, 9, 10],
695
706
  [11, 12],
@@ -702,7 +713,7 @@ describe('Arr transformations', () => {
702
713
 
703
714
  const result = partition(numbers, 2);
704
715
 
705
- assert.deepStrictEqual(result, [
716
+ assert.deepStrictEqual<readonly (readonly number[])[]>(result, [
706
717
  [1, 2],
707
718
  [3, 4],
708
719
  [5, 6],
@@ -714,7 +725,11 @@ describe('Arr transformations', () => {
714
725
 
715
726
  const result = partition(numbers, 2);
716
727
 
717
- assert.deepStrictEqual(result, [[1, 2], [3, 4], [5]]);
728
+ assert.deepStrictEqual<readonly (readonly number[])[]>(result, [
729
+ [1, 2],
730
+ [3, 4],
731
+ [5],
732
+ ]);
718
733
  });
719
734
 
720
735
  test('should work with chunk size < 2 (returns empty)', () => {
@@ -731,7 +746,7 @@ describe('Arr transformations', () => {
731
746
 
732
747
  const result = partition(numbers, 5);
733
748
 
734
- assert.deepStrictEqual(result, [[1, 2]]);
749
+ assert.deepStrictEqual<readonly (readonly number[])[]>(result, [[1, 2]]);
735
750
  });
736
751
 
737
752
  test('partition should work with empty array', () => {
@@ -1162,3 +1177,34 @@ describe('Arr transformations', () => {
1162
1177
  });
1163
1178
  });
1164
1179
  });
1180
+
1181
+ /* `toReversed` is a known permutation, so the tuple half can be reversed
1182
+ alongside the brand. It used to fall back to `ChangeArrayElement`, which kept
1183
+ the length but collapsed the positions to their union. Never called; see the
1184
+ note in `array-utils-modification.test.mts`. */
1185
+ export const reversedTypeChecks = (
1186
+ brandedFive: MinLengthArray<5, string>,
1187
+ brandedAndTuple: MinLengthArray<3, number> & readonly [1, 2, 3, 4, 5],
1188
+ ): void => {
1189
+ const _reversedBranded = toReversed(brandedFive);
1190
+
1191
+ expectType<typeof _reversedBranded, MinLengthArray<5, string>>('~=');
1192
+
1193
+ // Brand and positions both survive, and the positions really are reversed.
1194
+ const _reversedBoth = toReversed(brandedAndTuple);
1195
+
1196
+ expectType<(typeof _reversedBoth)[0], 5>('=');
1197
+
1198
+ expectType<(typeof _reversedBoth)[4], 1>('=');
1199
+
1200
+ const _reversedTuple = toReversed([1, 2, 3] as const);
1201
+
1202
+ expectType<typeof _reversedTuple, readonly [3, 2, 1]>('=');
1203
+
1204
+ /* `toSorted` deliberately does NOT take this path: an arbitrary permutation
1205
+ cannot keep positions, so the honest answer stays "same length, element
1206
+ type widened to the union". */
1207
+ const _sortedTuple = toSorted([1, 2, 3] as const);
1208
+
1209
+ expectType<typeof _sortedTuple, FixedLengthTuple<3, 1 | 2 | 3>>('~=');
1210
+ };