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
@@ -0,0 +1,364 @@
1
+ import {
2
+ type BoundedLengthTuple,
3
+ type FixedLengthTuple,
4
+ type MaxLengthTuple,
5
+ type MinLengthTuple,
6
+ } from 'ts-type-forge';
7
+ import { type SizeType } from '../../types.mjs';
8
+ import {
9
+ isBoundedLengthTuple,
10
+ isEmptyTuple,
11
+ isFixedLengthTuple,
12
+ isMaxLengthTuple,
13
+ isMinLengthTuple,
14
+ } from './array-utils-length-bounded-tuple-guard.mjs';
15
+
16
+ /**
17
+ * Casts an array to `FixedLengthTuple<Length, E>` after checking that it has
18
+ * exactly `length` elements.
19
+ *
20
+ * This is the structural counterpart of `asFixedLengthArray`: it produces a
21
+ * tuple type rather than the branded `FixedLengthArray`. Prefer the branded
22
+ * one unless you specifically need the structural type — the brand keeps
23
+ * type-checking cost independent of the element type and of the bound.
24
+ *
25
+ * The length argument comes first, matching the type-parameter order of
26
+ * `FixedLengthTuple<Length, Elm>`. Omitting the array returns a function that
27
+ * casts one.
28
+ *
29
+ * @example
30
+ *
31
+ * ```ts
32
+ * const rgb = Arr.asFixedLengthTuple(3, [255, 128, 0]);
33
+ *
34
+ * const red: number = rgb[0]; // OK — no `undefined`
35
+ *
36
+ * // curried version
37
+ * const asRgb = Arr.asFixedLengthTuple(3);
38
+ *
39
+ * // Arr.asFixedLengthTuple(3, [255, 128]); // throws TypeError
40
+ * ```
41
+ *
42
+ * @template Length - The exact number of elements.
43
+ * @template Xs - The input array type (tuple types are preserved).
44
+ * @param length - The exact number of elements.
45
+ * @param xs - The array to cast.
46
+ * @returns `xs` typed as `FixedLengthTuple<Length, Xs[number]> & Xs`.
47
+ * @throws {TypeError} If `xs.length !== length`.
48
+ */
49
+ export function asFixedLengthTuple<
50
+ Length extends SizeType.ArgArr,
51
+ Xs extends readonly unknown[],
52
+ >(length: Length, xs: Xs): FixedLengthTuple<Length, Xs[number]> & Xs;
53
+
54
+ // Curried version
55
+ export function asFixedLengthTuple<Length extends SizeType.ArgArr>(
56
+ length: Length,
57
+ ): <Xs extends readonly unknown[]>(
58
+ xs: Xs,
59
+ ) => FixedLengthTuple<Length, Xs[number]> & Xs;
60
+
61
+ export function asFixedLengthTuple(
62
+ ...args:
63
+ | readonly [length: SizeType.ArgArr, xs: readonly unknown[]]
64
+ | readonly [length: SizeType.ArgArr]
65
+ ): readonly unknown[] | ((xs: readonly unknown[]) => readonly unknown[]) {
66
+ switch (args.length) {
67
+ case 2:
68
+ return asFixedLengthTupleImpl(...args);
69
+
70
+ case 1:
71
+ return (xs) => asFixedLengthTupleImpl(args[0], xs);
72
+ }
73
+ }
74
+
75
+ const asFixedLengthTupleImpl = (
76
+ length: SizeType.ArgArr,
77
+ xs: readonly unknown[],
78
+ ): readonly unknown[] => {
79
+ const actualLength = xs.length;
80
+
81
+ if (!isFixedLengthTuple(length, xs)) {
82
+ throw new TypeError(
83
+ `Expected an array of length ${length}, got an array of length ${actualLength}`,
84
+ );
85
+ }
86
+
87
+ return xs;
88
+ };
89
+
90
+ /**
91
+ * Casts an array to `MinLengthTuple<MinLength, E>` after checking that it has
92
+ * at least `minLength` elements.
93
+ *
94
+ * This is the structural counterpart of `asMinLengthArray`; see
95
+ * {@link asFixedLengthTuple} for when to prefer which.
96
+ *
97
+ * @example
98
+ *
99
+ * ```ts
100
+ * const history = Arr.asMinLengthTuple(3, [0, 1, 2, 3]);
101
+ *
102
+ * const first: number = history[0]; // OK — no `undefined`
103
+ *
104
+ * // curried version
105
+ * const asHistory = Arr.asMinLengthTuple(3);
106
+ *
107
+ * // Arr.asMinLengthTuple(3, [0, 1]); // throws TypeError
108
+ * ```
109
+ *
110
+ * @template MinLength - The minimum number of elements (inclusive).
111
+ * @template Xs - The input array type (tuple types are preserved).
112
+ * @param minLength - The minimum number of elements (inclusive).
113
+ * @param xs - The array to cast.
114
+ * @returns `xs` typed as `MinLengthTuple<MinLength, Xs[number]> & Xs`.
115
+ * @throws {TypeError} If `xs.length < minLength`.
116
+ */
117
+ export function asMinLengthTuple<
118
+ MinLength extends SizeType.ArgArr,
119
+ Xs extends readonly unknown[],
120
+ >(minLength: MinLength, xs: Xs): MinLengthTuple<MinLength, Xs[number]> & Xs;
121
+
122
+ // Curried version
123
+ export function asMinLengthTuple<MinLength extends SizeType.ArgArr>(
124
+ minLength: MinLength,
125
+ ): <Xs extends readonly unknown[]>(
126
+ xs: Xs,
127
+ ) => MinLengthTuple<MinLength, Xs[number]> & Xs;
128
+
129
+ export function asMinLengthTuple(
130
+ ...args:
131
+ | readonly [minLength: SizeType.ArgArr, xs: readonly unknown[]]
132
+ | readonly [minLength: SizeType.ArgArr]
133
+ ): readonly unknown[] | ((xs: readonly unknown[]) => readonly unknown[]) {
134
+ switch (args.length) {
135
+ case 2:
136
+ return asMinLengthTupleImpl(...args);
137
+
138
+ case 1:
139
+ return (xs) => asMinLengthTupleImpl(args[0], xs);
140
+ }
141
+ }
142
+
143
+ const asMinLengthTupleImpl = (
144
+ minLength: SizeType.ArgArr,
145
+ xs: readonly unknown[],
146
+ ): readonly unknown[] => {
147
+ const actualLength = xs.length;
148
+
149
+ if (!isMinLengthTuple(minLength, xs)) {
150
+ throw new TypeError(
151
+ `Expected an array of length >= ${minLength}, got an array of length ${actualLength}`,
152
+ );
153
+ }
154
+
155
+ return xs;
156
+ };
157
+
158
+ /**
159
+ * Casts an array to `MaxLengthTuple<MaxLength, E>` after checking that it has
160
+ * at most `maxLength` elements.
161
+ *
162
+ * This is the structural counterpart of `asMaxLengthArray`; see
163
+ * {@link asFixedLengthTuple} for when to prefer which.
164
+ *
165
+ * @example
166
+ *
167
+ * ```ts
168
+ * const tags = Arr.asMaxLengthTuple(8, ['a', 'b', 'c']);
169
+ *
170
+ * // curried version
171
+ * const asTags = Arr.asMaxLengthTuple(8);
172
+ *
173
+ * // Arr.asMaxLengthTuple(2, ['a', 'b', 'c']); // throws TypeError
174
+ * ```
175
+ *
176
+ * @template MaxLength - The maximum number of elements (inclusive).
177
+ * @template Xs - The input array type (tuple types are preserved).
178
+ * @param maxLength - The maximum number of elements (inclusive).
179
+ * @param xs - The array to cast.
180
+ * @returns `xs` typed as `MaxLengthTuple<MaxLength, Xs[number]> & Xs`.
181
+ * @throws {TypeError} If `xs.length > maxLength`.
182
+ */
183
+ export function asMaxLengthTuple<
184
+ MaxLength extends SizeType.ArgArr,
185
+ Xs extends readonly unknown[],
186
+ >(maxLength: MaxLength, xs: Xs): MaxLengthTuple<MaxLength, Xs[number]> & Xs;
187
+
188
+ // Curried version
189
+ export function asMaxLengthTuple<MaxLength extends SizeType.ArgArr>(
190
+ maxLength: MaxLength,
191
+ ): <Xs extends readonly unknown[]>(
192
+ xs: Xs,
193
+ ) => MaxLengthTuple<MaxLength, Xs[number]> & Xs;
194
+
195
+ export function asMaxLengthTuple(
196
+ ...args:
197
+ | readonly [maxLength: SizeType.ArgArr, xs: readonly unknown[]]
198
+ | readonly [maxLength: SizeType.ArgArr]
199
+ ): readonly unknown[] | ((xs: readonly unknown[]) => readonly unknown[]) {
200
+ switch (args.length) {
201
+ case 2:
202
+ return asMaxLengthTupleImpl(...args);
203
+
204
+ case 1:
205
+ return (xs) => asMaxLengthTupleImpl(args[0], xs);
206
+ }
207
+ }
208
+
209
+ const asMaxLengthTupleImpl = (
210
+ maxLength: SizeType.ArgArr,
211
+ xs: readonly unknown[],
212
+ ): readonly unknown[] => {
213
+ const actualLength = xs.length;
214
+
215
+ if (!isMaxLengthTuple(maxLength, xs)) {
216
+ throw new TypeError(
217
+ `Expected an array of length <= ${maxLength}, got an array of length ${actualLength}`,
218
+ );
219
+ }
220
+
221
+ return xs;
222
+ };
223
+
224
+ /**
225
+ * Casts an array to `BoundedLengthTuple<Min, Max, E>` after checking that its
226
+ * length is within the inclusive range `[min, max]`.
227
+ *
228
+ * This is the structural counterpart of `asBoundedLengthArray`; see
229
+ * {@link asFixedLengthTuple} for when to prefer which.
230
+ *
231
+ * @example
232
+ *
233
+ * ```ts
234
+ * const selection = Arr.asBoundedLengthTuple(1, 5, [1, 2, 3]);
235
+ *
236
+ * // curried version
237
+ * const asSelection = Arr.asBoundedLengthTuple(1, 5);
238
+ *
239
+ * // Arr.asBoundedLengthTuple(1, 2, [1, 2, 3]); // throws TypeError
240
+ * ```
241
+ *
242
+ * @template Min - The minimum number of elements (inclusive).
243
+ * @template Max - The maximum number of elements (inclusive).
244
+ * @template Xs - The input array type (tuple types are preserved).
245
+ * @param min - The minimum number of elements (inclusive).
246
+ * @param max - The maximum number of elements (inclusive).
247
+ * @param xs - The array to cast.
248
+ * @returns `xs` typed as `BoundedLengthTuple<Min, Max, Xs[number]> & Xs`.
249
+ * @throws {TypeError} If `xs.length` is outside `[min, max]`.
250
+ */
251
+ export function asBoundedLengthTuple<
252
+ Min extends SizeType.ArgArr,
253
+ Max extends SizeType.ArgArr,
254
+ Xs extends readonly unknown[],
255
+ >(min: Min, max: Max, xs: Xs): BoundedLengthTuple<Min, Max, Xs[number]> & Xs;
256
+
257
+ // Curried version
258
+ export function asBoundedLengthTuple<
259
+ Min extends SizeType.ArgArr,
260
+ Max extends SizeType.ArgArr,
261
+ >(
262
+ min: Min,
263
+ max: Max,
264
+ ): <Xs extends readonly unknown[]>(
265
+ xs: Xs,
266
+ ) => BoundedLengthTuple<Min, Max, Xs[number]> & Xs;
267
+
268
+ export function asBoundedLengthTuple(
269
+ ...args:
270
+ | readonly [
271
+ min: SizeType.ArgArr,
272
+ max: SizeType.ArgArr,
273
+ xs: readonly unknown[],
274
+ ]
275
+ | readonly [min: SizeType.ArgArr, max: SizeType.ArgArr]
276
+ ): readonly unknown[] | ((xs: readonly unknown[]) => readonly unknown[]) {
277
+ switch (args.length) {
278
+ case 3:
279
+ return asBoundedLengthTupleImpl(...args);
280
+
281
+ case 2:
282
+ return (xs) => asBoundedLengthTupleImpl(...args, xs);
283
+ }
284
+ }
285
+
286
+ const asBoundedLengthTupleImpl = (
287
+ min: SizeType.ArgArr,
288
+ max: SizeType.ArgArr,
289
+ xs: readonly unknown[],
290
+ ): readonly unknown[] => {
291
+ const actualLength = xs.length;
292
+
293
+ if (!isBoundedLengthTuple(min, max, xs)) {
294
+ throw new TypeError(
295
+ `Expected an array of length in [${min}, ${max}], got an array of length ${actualLength}`,
296
+ );
297
+ }
298
+
299
+ return xs;
300
+ };
301
+
302
+ /**
303
+ * Casts an array to the structural empty tuple after checking that it is
304
+ * empty.
305
+ *
306
+ * This is the structural counterpart of `asEmptyArray`, and the length-0
307
+ * specialization of {@link asFixedLengthTuple}.
308
+ *
309
+ * @example
310
+ *
311
+ * ```ts
312
+ * const nothing = Arr.asEmptyTuple([]);
313
+ *
314
+ * // Arr.asEmptyTuple([1]); // throws TypeError
315
+ * ```
316
+ *
317
+ * @template Xs - The input array type (tuple types are preserved).
318
+ * @param xs - The array to cast.
319
+ * @returns `xs` typed as `readonly [] & Xs`.
320
+ * @throws {TypeError} If `xs` is not empty.
321
+ */
322
+ export const asEmptyTuple = <Xs extends readonly unknown[]>(
323
+ xs: Xs,
324
+ ): readonly [] & Xs => {
325
+ if (!isEmptyTuple(xs)) {
326
+ throw new TypeError(
327
+ `Expected an empty array, got an array of length ${xs.length}`,
328
+ );
329
+ }
330
+
331
+ return xs;
332
+ };
333
+
334
+ /**
335
+ * Casts an array to `MinLengthTuple<1, E>` after checking that it has at least
336
+ * one element.
337
+ *
338
+ * This is the structural counterpart of `asNonEmptyArray`, and the length-1
339
+ * specialization of {@link asMinLengthTuple}.
340
+ *
341
+ * @example
342
+ *
343
+ * ```ts
344
+ * const values = Arr.asNonEmptyTuple([1, 2, 3]);
345
+ *
346
+ * const first: number = values[0]; // OK — no `undefined`
347
+ *
348
+ * // Arr.asNonEmptyTuple([]); // throws TypeError
349
+ * ```
350
+ *
351
+ * @template Xs - The input array type (tuple types are preserved).
352
+ * @param xs - The array to cast.
353
+ * @returns `xs` typed as `MinLengthTuple<1, Xs[number]> & Xs`.
354
+ * @throws {TypeError} If `xs` is empty.
355
+ */
356
+ export const asNonEmptyTuple = <Xs extends readonly unknown[]>(
357
+ xs: Xs,
358
+ ): MinLengthTuple<1, Xs[number]> & Xs => {
359
+ if (!isMinLengthTuple(1, xs)) {
360
+ throw new TypeError('Expected a non-empty array, got an empty array');
361
+ }
362
+
363
+ return xs;
364
+ };
@@ -0,0 +1,207 @@
1
+ import {
2
+ type BoundedLengthTuple,
3
+ type FixedLengthTuple,
4
+ type MaxLengthTuple,
5
+ type MinLengthTuple,
6
+ } from 'ts-type-forge';
7
+ import { expectType } from '../../expect-type.mjs';
8
+ import {
9
+ asBoundedLengthTuple,
10
+ asEmptyTuple,
11
+ asFixedLengthTuple,
12
+ asMaxLengthTuple,
13
+ asMinLengthTuple,
14
+ asNonEmptyTuple,
15
+ } from './array-utils-length-bounded-tuple-cast.mjs';
16
+
17
+ describe(asFixedLengthTuple, () => {
18
+ test('casts to the structural tuple, so indexed access is defined', () => {
19
+ const values: readonly number[] = [255, 128, 0] as const;
20
+
21
+ const casted = asFixedLengthTuple(3, values);
22
+
23
+ expectType<typeof casted, FixedLengthTuple<3, number>>('~=');
24
+
25
+ const red: number = casted[0];
26
+
27
+ assert.deepStrictEqual(red, 255);
28
+ });
29
+
30
+ test('is the same value it was handed', () => {
31
+ const values: readonly number[] = [1, 2] as const;
32
+
33
+ expect(asFixedLengthTuple(2, values)).toBe(values);
34
+ });
35
+
36
+ test('throws on a length mismatch', () => {
37
+ expect(() => asFixedLengthTuple(3, [255, 128])).toThrow(TypeError);
38
+
39
+ expect(() => asFixedLengthTuple(3, [255, 128])).toThrow(
40
+ 'Expected an array of length 3, got an array of length 2',
41
+ );
42
+ });
43
+
44
+ test('curried form applies the same check later', () => {
45
+ const asRgb = asFixedLengthTuple(3);
46
+
47
+ const casted = asRgb([1, 2, 3] as const);
48
+
49
+ // The input type is carried over, so the literal positions survive the cast
50
+ // exactly as they do for `asFixedLengthArray`.
51
+ expectType<
52
+ typeof casted,
53
+ FixedLengthTuple<3, 1 | 2 | 3> & readonly [1, 2, 3]
54
+ >('~=');
55
+
56
+ assert.deepStrictEqual<readonly number[]>(casted, [1, 2, 3]);
57
+
58
+ expect(() => asRgb([1, 2])).toThrow(TypeError);
59
+ });
60
+ });
61
+
62
+ describe(asMinLengthTuple, () => {
63
+ test('casts to the structural minimum-length tuple', () => {
64
+ const values: readonly number[] = [0, 1, 2, 3] as const;
65
+
66
+ const casted = asMinLengthTuple(3, values);
67
+
68
+ expectType<typeof casted, MinLengthTuple<3, number>>('~=');
69
+
70
+ const first: number = casted[0];
71
+
72
+ assert.deepStrictEqual(first, 0);
73
+ });
74
+
75
+ test('throws when the array is too short', () => {
76
+ expect(() => asMinLengthTuple(3, [0, 1])).toThrow(
77
+ 'Expected an array of length >= 3, got an array of length 2',
78
+ );
79
+ });
80
+
81
+ test('curried form applies the same check later', () => {
82
+ const asHistory = asMinLengthTuple(3);
83
+
84
+ assert.deepStrictEqual(asHistory([1, 2, 3]), [1, 2, 3]);
85
+
86
+ expect(() => asHistory([1])).toThrow(TypeError);
87
+ });
88
+ });
89
+
90
+ describe(asMaxLengthTuple, () => {
91
+ test('casts to the structural maximum-length tuple', () => {
92
+ const values: readonly string[] = ['a', 'b', 'c'] as const;
93
+
94
+ const casted = asMaxLengthTuple(8, values);
95
+
96
+ expectType<typeof casted, MaxLengthTuple<8, string>>('~=');
97
+
98
+ assert.deepStrictEqual<readonly string[]>(casted, ['a', 'b', 'c']);
99
+ });
100
+
101
+ test('throws when the array is too long', () => {
102
+ expect(() => asMaxLengthTuple(2, ['a', 'b', 'c'])).toThrow(
103
+ 'Expected an array of length <= 2, got an array of length 3',
104
+ );
105
+ });
106
+
107
+ test('curried form applies the same check later', () => {
108
+ const asTags = asMaxLengthTuple(2);
109
+
110
+ assert.deepStrictEqual(asTags(['a']), ['a']);
111
+
112
+ expect(() => asTags(['a', 'b', 'c'])).toThrow(TypeError);
113
+ });
114
+ });
115
+
116
+ describe(asBoundedLengthTuple, () => {
117
+ test('casts to the structural bounded-length tuple', () => {
118
+ const values: readonly number[] = [1, 2, 3] as const;
119
+
120
+ const casted = asBoundedLengthTuple(1, 5, values);
121
+
122
+ expectType<typeof casted, BoundedLengthTuple<1, 5, number>>('~=');
123
+
124
+ assert.deepStrictEqual<readonly number[]>(casted, [1, 2, 3]);
125
+ });
126
+
127
+ test('throws when the length is outside the range', () => {
128
+ expect(() => asBoundedLengthTuple(1, 2, [1, 2, 3])).toThrow(
129
+ 'Expected an array of length in [1, 2], got an array of length 3',
130
+ );
131
+ });
132
+
133
+ test('curried form applies the same check later', () => {
134
+ const asSelection = asBoundedLengthTuple(1, 2);
135
+
136
+ assert.deepStrictEqual(asSelection([1]), [1]);
137
+
138
+ expect(() => asSelection([1, 2, 3])).toThrow(TypeError);
139
+ });
140
+ });
141
+
142
+ describe(asEmptyTuple, () => {
143
+ test('casts an empty array to the empty tuple', () => {
144
+ const values: readonly number[] = [] as const;
145
+
146
+ const casted = asEmptyTuple(values);
147
+
148
+ expectType<typeof casted, readonly []>('~=');
149
+
150
+ // Like the other casts, this hands back the value it was given.
151
+ expect(casted).toBe(values);
152
+ });
153
+
154
+ test('throws when the array is not empty', () => {
155
+ expect(() => asEmptyTuple([1])).toThrow(
156
+ 'Expected an empty array, got an array of length 1',
157
+ );
158
+ });
159
+ });
160
+
161
+ describe(asNonEmptyTuple, () => {
162
+ test('casts to the structural non-empty tuple', () => {
163
+ const values: readonly number[] = [1, 2, 3] as const;
164
+
165
+ const casted = asNonEmptyTuple(values);
166
+
167
+ expectType<typeof casted, MinLengthTuple<1, number>>('~=');
168
+
169
+ const first: number = casted[0];
170
+
171
+ assert.deepStrictEqual(first, 1);
172
+ });
173
+
174
+ test('throws when the array is empty', () => {
175
+ expect(() => asNonEmptyTuple([])).toThrow(
176
+ 'Expected a non-empty array, got an empty array',
177
+ );
178
+ });
179
+ });
180
+
181
+ describe('tuple casts preserve the input type, like their branded counterparts', () => {
182
+ test('an exact tuple survives asMinLengthTuple', () => {
183
+ const values = [1, 2, 3, 4, 5] as const;
184
+
185
+ const casted = asMinLengthTuple(3, values);
186
+
187
+ // Before, this widened to `MinLengthTuple<3, 1 | 2 | 3 | 4 | 5>` and the
188
+ // exact length was gone; `asMinLengthArray` never lost it.
189
+ expectType<typeof casted, readonly [1, 2, 3, 4, 5]>('~=');
190
+
191
+ const length: 5 = casted.length;
192
+
193
+ assert.deepStrictEqual(length, 5);
194
+
195
+ assert.deepStrictEqual<readonly number[]>(casted, [1, 2, 3, 4, 5]);
196
+ });
197
+
198
+ test('asNonEmptyTuple keeps the tuple too', () => {
199
+ const casted = asNonEmptyTuple([1, 2, 3] as const);
200
+
201
+ expectType<typeof casted, readonly [1, 2, 3]>('~=');
202
+
203
+ const first: 1 = casted[0];
204
+
205
+ assert.deepStrictEqual(first, 1);
206
+ });
207
+ });