ts-data-forge 9.0.1 → 11.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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/array/impl/array-utils-creation.d.mts +4 -4
  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-element-access.d.mts +4 -4
  6. package/dist/array/impl/array-utils-element-access.mjs.map +1 -1
  7. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts +117 -0
  8. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts.map +1 -0
  9. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs +37 -0
  10. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs.map +1 -0
  11. package/dist/array/impl/array-utils-length-bounded-array-guard.d.mts +159 -0
  12. package/dist/array/impl/array-utils-length-bounded-array-guard.d.mts.map +1 -0
  13. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs +7 -0
  14. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs.map +1 -0
  15. package/dist/array/impl/array-utils-modification.d.mts +11 -11
  16. package/dist/array/impl/array-utils-modification.d.mts.map +1 -1
  17. package/dist/array/impl/array-utils-modification.mjs.map +1 -1
  18. package/dist/array/impl/array-utils-reducing-value.d.mts +9 -9
  19. package/dist/array/impl/array-utils-reducing-value.mjs.map +1 -1
  20. package/dist/array/impl/array-utils-size.d.mts +3 -3
  21. package/dist/array/impl/array-utils-size.mjs.map +1 -1
  22. package/dist/array/impl/array-utils-slicing.d.mts +9 -9
  23. package/dist/array/impl/array-utils-slicing.d.mts.map +1 -1
  24. package/dist/array/impl/array-utils-slicing.mjs.map +1 -1
  25. package/dist/array/impl/array-utils-transformation.d.mts +16 -10
  26. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  27. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  28. package/dist/array/impl/array-utils-validation.d.mts +17 -17
  29. package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
  30. package/dist/array/impl/array-utils-validation.mjs +5 -5
  31. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  32. package/dist/array/impl/index.d.mts +2 -0
  33. package/dist/array/impl/index.d.mts.map +1 -1
  34. package/dist/array/impl/index.mjs +3 -1
  35. package/dist/array/impl/index.mjs.map +1 -1
  36. package/dist/entry-point.mjs +1 -1
  37. package/dist/guard/index.d.mts +0 -1
  38. package/dist/guard/index.d.mts.map +1 -1
  39. package/dist/guard/index.mjs +0 -1
  40. package/dist/guard/index.mjs.map +1 -1
  41. package/dist/index.d.mts +1 -0
  42. package/dist/index.d.mts.map +1 -1
  43. package/dist/index.mjs +1 -1
  44. package/dist/string/index.d.mts +2 -0
  45. package/dist/string/index.d.mts.map +1 -0
  46. package/dist/string/index.mjs +2 -0
  47. package/dist/string/index.mjs.map +1 -0
  48. package/dist/string/str.d.mts +273 -0
  49. package/dist/string/str.d.mts.map +1 -0
  50. package/dist/string/str.mjs +42 -0
  51. package/dist/string/str.mjs.map +1 -0
  52. package/package.json +30 -92
  53. package/src/array/impl/array-utils-creation.mts +5 -4
  54. package/src/array/impl/array-utils-element-access.mts +3 -3
  55. package/src/array/impl/array-utils-element-access.test.mts +3 -3
  56. package/src/array/impl/array-utils-length-bounded-array-cast.mts +190 -0
  57. package/src/array/impl/array-utils-length-bounded-array-cast.test.mts +118 -0
  58. package/src/array/impl/array-utils-length-bounded-array-guard.mts +195 -0
  59. package/src/array/impl/array-utils-length-bounded-array-guard.test.mts +169 -0
  60. package/src/array/impl/array-utils-modification.mts +16 -15
  61. package/src/array/impl/array-utils-modification.test.mts +10 -5
  62. package/src/array/impl/array-utils-reducing-value.mts +9 -9
  63. package/src/array/impl/array-utils-size.mts +2 -2
  64. package/src/array/impl/array-utils-slicing.mts +24 -13
  65. package/src/array/impl/array-utils-transformation.mts +22 -16
  66. package/src/array/impl/array-utils-transformation.test.mts +19 -19
  67. package/src/array/impl/array-utils-validation.mts +24 -24
  68. package/src/array/impl/array-utils-validation.test.mts +94 -92
  69. package/src/array/impl/index.mts +2 -0
  70. package/src/collections/queue.test.mts +1 -1
  71. package/src/guard/index.mts +0 -1
  72. package/src/index.mts +1 -0
  73. package/src/object/object.test.mts +3 -3
  74. package/src/string/index.mts +1 -0
  75. package/src/string/str.mts +371 -0
  76. package/src/string/str.test.mts +215 -0
  77. package/dist/guard/is-length-bounded-string.d.mts +0 -164
  78. package/dist/guard/is-length-bounded-string.d.mts.map +0 -1
  79. package/dist/guard/is-length-bounded-string.mjs +0 -7
  80. package/dist/guard/is-length-bounded-string.mjs.map +0 -1
  81. package/src/guard/is-length-bounded-string.mts +0 -199
  82. package/src/guard/is-length-bounded-string.test.mts +0 -170
@@ -0,0 +1,190 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type MaxLengthArray,
5
+ type MinLengthArray,
6
+ type SupportedLength,
7
+ } from 'ts-type-forge';
8
+ import {
9
+ isBoundedLengthArray,
10
+ isFixedLengthArray,
11
+ isMaxLengthArray,
12
+ isMinLengthArray,
13
+ } from './array-utils-length-bounded-array-guard.mjs';
14
+
15
+ /**
16
+ * Casts an array to `FixedLengthArray<Length, E>` after checking that it has
17
+ * exactly `length` elements.
18
+ *
19
+ * Use this instead of writing `xs as unknown as FixedLengthArray<N, E>`: the
20
+ * length is verified at runtime, and the original array type (e.g. tuple
21
+ * types) is preserved via intersection, so the result also benefits from the
22
+ * structural tuple prefix (in-range indexed access without `undefined` under
23
+ * `noUncheckedIndexedAccess`).
24
+ *
25
+ * @example
26
+ *
27
+ * ```ts
28
+ * const rgb = Arr.asFixedLengthArray([255, 128, 0], 3);
29
+ *
30
+ * const atMost5: MaxLengthArray<5, number> = rgb; // OK (3 <= 5)
31
+ * const red: number = rgb[0]; // OK — no `undefined`
32
+ *
33
+ * // Arr.asFixedLengthArray([255, 128], 3); // throws TypeError
34
+ * ```
35
+ *
36
+ * @template Xs - The input array type (tuple types are preserved).
37
+ * @template Length - The exact number of elements.
38
+ * @param xs - The array to cast.
39
+ * @param length - The exact number of elements.
40
+ * @returns `xs` typed as `FixedLengthArray<Length, Xs[number]> & Xs`.
41
+ * @throws {TypeError} If `xs.length !== length`.
42
+ */
43
+ export const asFixedLengthArray = <
44
+ Xs extends readonly unknown[],
45
+ Length extends SupportedLength,
46
+ >(
47
+ xs: Xs,
48
+ length: Length,
49
+ ): FixedLengthArray<Length, Xs[number]> & Xs => {
50
+ if (!isFixedLengthArray(xs, length)) {
51
+ throw new TypeError(
52
+ `Expected an array of length ${length}, got an array of length ${xs.length}`,
53
+ );
54
+ }
55
+
56
+ return xs;
57
+ };
58
+
59
+ /**
60
+ * Casts an array to `MinLengthArray<MinLength, E>` after checking that it has
61
+ * at least `minLength` elements.
62
+ *
63
+ * Use this instead of writing `xs as unknown as MinLengthArray<N, E>`: the
64
+ * length is verified at runtime, and the original array type (e.g. tuple
65
+ * types) is preserved via intersection, so the result also benefits from the
66
+ * structural tuple prefix (indexed access below `min(N, 10)` without
67
+ * `undefined` under `noUncheckedIndexedAccess`).
68
+ *
69
+ * @example
70
+ *
71
+ * ```ts
72
+ * const history = Arr.asMinLengthArray([0, 1, 2, 3], 3);
73
+ *
74
+ * const nonEmpty: MinLengthArray<1, number> = history; // OK (3 >= 1)
75
+ * const first: number = history[0]; // OK — no `undefined`
76
+ *
77
+ * // Arr.asMinLengthArray([0], 3); // throws TypeError
78
+ * ```
79
+ *
80
+ * @template Xs - The input array type (tuple types are preserved).
81
+ * @template MinLength - The minimum number of elements (inclusive).
82
+ * @param xs - The array to cast.
83
+ * @param minLength - The minimum number of elements (inclusive).
84
+ * @returns `xs` typed as `MinLengthArray<MinLength, Xs[number]> & Xs`.
85
+ * @throws {TypeError} If `xs.length < minLength`.
86
+ */
87
+ export const asMinLengthArray = <
88
+ Xs extends readonly unknown[],
89
+ MinLength extends SupportedLength,
90
+ >(
91
+ xs: Xs,
92
+ minLength: MinLength,
93
+ ): MinLengthArray<MinLength, Xs[number]> & Xs => {
94
+ if (!isMinLengthArray(xs, minLength)) {
95
+ throw new TypeError(
96
+ `Expected an array of length >= ${minLength}, got an array of length ${xs.length}`,
97
+ );
98
+ }
99
+
100
+ return xs;
101
+ };
102
+
103
+ /**
104
+ * Casts an array to `MaxLengthArray<MaxLength, E>` after checking that it has
105
+ * at most `maxLength` elements.
106
+ *
107
+ * Use this instead of writing `xs as unknown as MaxLengthArray<N, E>`: the
108
+ * length is verified at runtime, and the original array type (e.g. tuple
109
+ * types) is preserved via intersection.
110
+ *
111
+ * @example
112
+ *
113
+ * ```ts
114
+ * const tags = Arr.asMaxLengthArray(['a', 'b', 'c'], 8);
115
+ *
116
+ * const relaxed: MaxLengthArray<16, string> = tags; // OK (8 <= 16)
117
+ *
118
+ * // Arr.asMaxLengthArray(['a', 'b', 'c'], 2); // throws TypeError
119
+ * ```
120
+ *
121
+ * @template Xs - The input array type (tuple types are preserved).
122
+ * @template MaxLength - The maximum number of elements (inclusive).
123
+ * @param xs - The array to cast.
124
+ * @param maxLength - The maximum number of elements (inclusive).
125
+ * @returns `xs` typed as `MaxLengthArray<MaxLength, Xs[number]> & Xs`.
126
+ * @throws {TypeError} If `xs.length > maxLength`.
127
+ */
128
+ export const asMaxLengthArray = <
129
+ Xs extends readonly unknown[],
130
+ MaxLength extends SupportedLength,
131
+ >(
132
+ xs: Xs,
133
+ maxLength: MaxLength,
134
+ ): MaxLengthArray<MaxLength, Xs[number]> & Xs => {
135
+ if (!isMaxLengthArray(xs, maxLength)) {
136
+ throw new TypeError(
137
+ `Expected an array of length <= ${maxLength}, got an array of length ${xs.length}`,
138
+ );
139
+ }
140
+
141
+ return xs;
142
+ };
143
+
144
+ /**
145
+ * Casts an array to `BoundedLengthArray<MinLength, MaxLength, E>` after
146
+ * checking that its length is within the inclusive range
147
+ * `[minLength, maxLength]`.
148
+ *
149
+ * Use this instead of writing
150
+ * `xs as unknown as BoundedLengthArray<Min, Max, E>`: the length is verified
151
+ * at runtime, and the original array type (e.g. tuple types) is preserved via
152
+ * intersection.
153
+ *
154
+ * @example
155
+ *
156
+ * ```ts
157
+ * const selection = Arr.asBoundedLengthArray([1, 2, 3], 1, 5);
158
+ *
159
+ * const relaxed: BoundedLengthArray<0, 100, number> = selection; // OK
160
+ *
161
+ * // Arr.asBoundedLengthArray([], 1, 5); // throws TypeError
162
+ * ```
163
+ *
164
+ * @template Xs - The input array type (tuple types are preserved).
165
+ * @template MinLength - The minimum number of elements (inclusive).
166
+ * @template MaxLength - The maximum number of elements (inclusive).
167
+ * @param xs - The array to cast.
168
+ * @param minLength - The minimum number of elements (inclusive).
169
+ * @param maxLength - The maximum number of elements (inclusive).
170
+ * @returns `xs` typed as
171
+ * `BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs`.
172
+ * @throws {TypeError} If `xs.length < minLength || xs.length > maxLength`.
173
+ */
174
+ export const asBoundedLengthArray = <
175
+ Xs extends readonly unknown[],
176
+ MinLength extends SupportedLength,
177
+ MaxLength extends SupportedLength,
178
+ >(
179
+ xs: Xs,
180
+ minLength: MinLength,
181
+ maxLength: MaxLength,
182
+ ): BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs => {
183
+ if (!isBoundedLengthArray(xs, minLength, maxLength)) {
184
+ throw new TypeError(
185
+ `Expected an array of length in [${minLength}, ${maxLength}], got an array of length ${xs.length}`,
186
+ );
187
+ }
188
+
189
+ return xs;
190
+ };
@@ -0,0 +1,118 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type MaxLengthArray,
5
+ type MinLengthArray,
6
+ } from 'ts-type-forge';
7
+ import { expectType } from '../../expect-type.mjs';
8
+ import {
9
+ asBoundedLengthArray,
10
+ asFixedLengthArray,
11
+ asMaxLengthArray,
12
+ asMinLengthArray,
13
+ } from './array-utils-length-bounded-array-cast.mjs';
14
+
15
+ describe(asFixedLengthArray, () => {
16
+ test('should return the input when the length matches exactly', () => {
17
+ const xs: readonly number[] = [0, 1, 2, 3] as const;
18
+
19
+ const casted = asFixedLengthArray(xs, 4);
20
+
21
+ expect(casted).toBe(xs);
22
+
23
+ expectType<typeof casted, FixedLengthArray<4, number>>('<=');
24
+
25
+ // in-range indexed access does not include `undefined`
26
+ const _first = casted[0];
27
+
28
+ expectType<typeof _first, number>('=');
29
+ });
30
+
31
+ test('should throw when the length does not match', () => {
32
+ expect(() => asFixedLengthArray([0, 1, 2], 4)).toThrow(TypeError);
33
+
34
+ expect(() => asFixedLengthArray([0, 1, 2], 4)).toThrow(
35
+ 'Expected an array of length 4, got an array of length 3',
36
+ );
37
+ });
38
+
39
+ test('should preserve the original array type', () => {
40
+ const tuple = [1, 2, 3] as const;
41
+
42
+ const casted = asFixedLengthArray(tuple, 3);
43
+
44
+ expect(casted).toBe(tuple);
45
+
46
+ expectType<typeof casted, readonly [1, 2, 3]>('<=');
47
+
48
+ expectType<typeof casted, FixedLengthArray<3, 1 | 2 | 3>>('<=');
49
+ });
50
+ });
51
+
52
+ describe(asMinLengthArray, () => {
53
+ test('should return the input when the array is long enough', () => {
54
+ const xs: readonly number[] = [0, 1, 2, 3] as const;
55
+
56
+ const casted = asMinLengthArray(xs, 3);
57
+
58
+ expect(casted).toBe(xs);
59
+
60
+ expectType<typeof casted, MinLengthArray<3, number>>('<=');
61
+
62
+ expectType<typeof casted, MinLengthArray<1, number>>('<='); // 3 >= 1
63
+ });
64
+
65
+ test('should throw when the array is too short', () => {
66
+ expect(() => asMinLengthArray([0], 3)).toThrow(TypeError);
67
+
68
+ expect(() => asMinLengthArray([0], 3)).toThrow(
69
+ 'Expected an array of length >= 3, got an array of length 1',
70
+ );
71
+ });
72
+ });
73
+
74
+ describe(asMaxLengthArray, () => {
75
+ test('should return the input when the array is short enough', () => {
76
+ const xs: readonly number[] = [0, 1, 2] as const;
77
+
78
+ const casted = asMaxLengthArray(xs, 8);
79
+
80
+ expect(casted).toBe(xs);
81
+
82
+ expectType<typeof casted, MaxLengthArray<8, number>>('<=');
83
+
84
+ expectType<typeof casted, MaxLengthArray<16, number>>('<='); // 8 <= 16
85
+ });
86
+
87
+ test('should throw when the array is too long', () => {
88
+ expect(() => asMaxLengthArray([0, 1, 2], 2)).toThrow(TypeError);
89
+
90
+ expect(() => asMaxLengthArray([0, 1, 2], 2)).toThrow(
91
+ 'Expected an array of length <= 2, got an array of length 3',
92
+ );
93
+ });
94
+ });
95
+
96
+ describe(asBoundedLengthArray, () => {
97
+ test('should return the input when the length is within the range', () => {
98
+ const xs: readonly number[] = [1, 2, 3] as const;
99
+
100
+ const casted = asBoundedLengthArray(xs, 1, 5);
101
+
102
+ expect(casted).toBe(xs);
103
+
104
+ expectType<typeof casted, BoundedLengthArray<1, 5, number>>('<=');
105
+
106
+ expectType<typeof casted, MinLengthArray<1, number>>('<=');
107
+
108
+ expectType<typeof casted, MaxLengthArray<5, number>>('<=');
109
+ });
110
+
111
+ test('should throw when the length is out of the range', () => {
112
+ expect(() => asBoundedLengthArray([], 1, 5)).toThrow(TypeError);
113
+
114
+ expect(() => asBoundedLengthArray([1, 2, 3, 4, 5, 6], 1, 5)).toThrow(
115
+ 'Expected an array of length in [1, 5], got an array of length 6',
116
+ );
117
+ });
118
+ });
@@ -0,0 +1,195 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type MaxLengthArray,
5
+ type MinLengthArray,
6
+ type SupportedLength,
7
+ } from 'ts-type-forge';
8
+
9
+ /**
10
+ * Type guard that checks if an array has at least `minLength` elements.
11
+ *
12
+ * **Type Narrowing Behavior:**
13
+ *
14
+ * - Narrows the input to `MinLengthArray<MinLength, E>` while preserving the
15
+ * original array type (e.g. tuple types) via intersection.
16
+ * - The length constraint lives only in the brand, so this stays cheap for the
17
+ * type checker even when the element type is large — unlike
18
+ * `isMinLengthTuple`, which narrows to a structural tuple type.
19
+ * - The result participates in the length-constraint subtyping relation: for
20
+ * example, a value narrowed to `MinLengthArray<3, E>` is assignable to
21
+ * `MinLengthArray<1, E>`.
22
+ *
23
+ * @example
24
+ *
25
+ * ```ts
26
+ * const input: readonly number[] = [0, 1, 2, 3];
27
+ *
28
+ * assert.isTrue(Arr.isMinLengthArray(input, 3));
29
+ *
30
+ * assert.isFalse(Arr.isMinLengthArray([0], 3));
31
+ *
32
+ * if (Arr.isMinLengthArray(input, 3)) {
33
+ * const history: MinLengthArray<3, number> = input;
34
+ *
35
+ * const nonEmpty: MinLengthArray<1, number> = input; // OK (3 >= 1)
36
+ * }
37
+ * ```
38
+ *
39
+ * @template Xs - The input array type (tuple types are preserved).
40
+ * @template MinLength - The minimum number of elements (inclusive).
41
+ * @param xs - The array to check.
42
+ * @param minLength - The minimum number of elements (inclusive).
43
+ * @returns `true` if `xs.length >= minLength`, `false` otherwise. When `true`,
44
+ * TypeScript narrows `xs` to `MinLengthArray<MinLength, Xs[number]> & Xs`.
45
+ */
46
+ export const isMinLengthArray = <
47
+ Xs extends readonly unknown[],
48
+ MinLength extends SupportedLength,
49
+ >(
50
+ xs: Xs,
51
+ minLength: MinLength,
52
+ ): xs is MinLengthArray<MinLength, Xs[number]> & Xs => xs.length >= minLength;
53
+
54
+ /**
55
+ * Type guard that checks if an array has at most `maxLength` elements.
56
+ *
57
+ * **Type Narrowing Behavior:**
58
+ *
59
+ * - Narrows the input to `MaxLengthArray<MaxLength, E>` while preserving the
60
+ * original array type (e.g. tuple types) via intersection.
61
+ * - The length constraint lives only in the brand, so this stays cheap for the
62
+ * type checker even when the element type is large — unlike
63
+ * `isMaxLengthTuple`, which narrows to a union of tuple types.
64
+ * - The result participates in the length-constraint subtyping relation: for
65
+ * example, a value narrowed to `MaxLengthArray<8, E>` is assignable to
66
+ * `MaxLengthArray<16, E>`.
67
+ *
68
+ * @example
69
+ *
70
+ * ```ts
71
+ * const input: readonly string[] = ['a', 'b', 'c'];
72
+ *
73
+ * assert.isTrue(Arr.isMaxLengthArray(input, 8));
74
+ *
75
+ * assert.isFalse(Arr.isMaxLengthArray(input, 2));
76
+ *
77
+ * if (Arr.isMaxLengthArray(input, 8)) {
78
+ * const tags: MaxLengthArray<8, string> = input;
79
+ *
80
+ * const relaxed: MaxLengthArray<16, string> = input; // OK (8 <= 16)
81
+ * }
82
+ * ```
83
+ *
84
+ * @template Xs - The input array type (tuple types are preserved).
85
+ * @template MaxLength - The maximum number of elements (inclusive).
86
+ * @param xs - The array to check.
87
+ * @param maxLength - The maximum number of elements (inclusive).
88
+ * @returns `true` if `xs.length <= maxLength`, `false` otherwise. When `true`,
89
+ * TypeScript narrows `xs` to `MaxLengthArray<MaxLength, Xs[number]> & Xs`.
90
+ */
91
+ export const isMaxLengthArray = <
92
+ Xs extends readonly unknown[],
93
+ MaxLength extends SupportedLength,
94
+ >(
95
+ xs: Xs,
96
+ maxLength: MaxLength,
97
+ ): xs is MaxLengthArray<MaxLength, Xs[number]> & Xs => xs.length <= maxLength;
98
+
99
+ /**
100
+ * Type guard that checks if an array's length is within the inclusive range
101
+ * `[minLength, maxLength]`.
102
+ *
103
+ * **Type Narrowing Behavior:**
104
+ *
105
+ * - Narrows the input to `BoundedLengthArray<MinLength, MaxLength, E>` while
106
+ * preserving the original array type (e.g. tuple types) via intersection.
107
+ * - Since `BoundedLengthArray` is the intersection of `MinLengthArray` and
108
+ * `MaxLengthArray`, the narrowed value is assignable to both, and both
109
+ * bounds can be weakened independently.
110
+ * - The length constraint lives only in the brand, so this stays cheap for the
111
+ * type checker even when the element type is large — unlike
112
+ * `isBoundedLengthTuple`, which narrows to a union of tuple types.
113
+ *
114
+ * @example
115
+ *
116
+ * ```ts
117
+ * const input: readonly number[] = [1, 2, 3];
118
+ *
119
+ * assert.isTrue(Arr.isBoundedLengthArray(input, 1, 5));
120
+ *
121
+ * assert.isFalse(Arr.isBoundedLengthArray([], 1, 5));
122
+ *
123
+ * if (Arr.isBoundedLengthArray(input, 1, 5)) {
124
+ * const selection: BoundedLengthArray<1, 5, number> = input;
125
+ *
126
+ * const relaxed: BoundedLengthArray<0, 100, number> = input; // OK ([1, 5] ⊆ [0, 100])
127
+ * }
128
+ * ```
129
+ *
130
+ * @template Xs - The input array type (tuple types are preserved).
131
+ * @template MinLength - The minimum number of elements (inclusive).
132
+ * @template MaxLength - The maximum number of elements (inclusive).
133
+ * @param xs - The array to check.
134
+ * @param minLength - The minimum number of elements (inclusive).
135
+ * @param maxLength - The maximum number of elements (inclusive).
136
+ * @returns `true` if `minLength <= xs.length && xs.length <= maxLength`,
137
+ * `false` otherwise. When `true`, TypeScript narrows `xs` to
138
+ * `BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs`.
139
+ */
140
+ export const isBoundedLengthArray = <
141
+ Xs extends readonly unknown[],
142
+ MinLength extends SupportedLength,
143
+ MaxLength extends SupportedLength,
144
+ >(
145
+ xs: Xs,
146
+ minLength: MinLength,
147
+ maxLength: MaxLength,
148
+ ): xs is BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs =>
149
+ xs.length >= minLength && xs.length <= maxLength;
150
+
151
+ /**
152
+ * Type guard that checks if an array has exactly `length` elements.
153
+ *
154
+ * **Type Narrowing Behavior:**
155
+ *
156
+ * - Narrows the input to `FixedLengthArray<Length, E>` while preserving the
157
+ * original array type (e.g. tuple types) via intersection.
158
+ * - Since `FixedLengthArray<Length, E>` is defined as
159
+ * `BoundedLengthArray<Length, Length, E>`, the narrowed value is assignable
160
+ * to any `MinLengthArray<N, E>` with `N <= Length` and any
161
+ * `MaxLengthArray<N, E>` with `N >= Length`.
162
+ * - The length constraint lives only in the brand, so this stays cheap for the
163
+ * type checker even when the element type is large — unlike
164
+ * `isFixedLengthTuple`, which narrows to a structural tuple type.
165
+ *
166
+ * @example
167
+ *
168
+ * ```ts
169
+ * const input: readonly number[] = [255, 128, 0];
170
+ *
171
+ * assert.isTrue(Arr.isFixedLengthArray(input, 3));
172
+ *
173
+ * assert.isFalse(Arr.isFixedLengthArray(input, 4));
174
+ *
175
+ * if (Arr.isFixedLengthArray(input, 3)) {
176
+ * const rgb: FixedLengthArray<3, number> = input;
177
+ *
178
+ * const atMost5: MaxLengthArray<5, number> = input; // OK (3 <= 5)
179
+ * }
180
+ * ```
181
+ *
182
+ * @template Xs - The input array type (tuple types are preserved).
183
+ * @template Length - The exact number of elements.
184
+ * @param xs - The array to check.
185
+ * @param length - The exact number of elements.
186
+ * @returns `true` if `xs.length === length`, `false` otherwise. When `true`,
187
+ * TypeScript narrows `xs` to `FixedLengthArray<Length, Xs[number]> & Xs`.
188
+ */
189
+ export const isFixedLengthArray = <
190
+ Xs extends readonly unknown[],
191
+ Length extends SupportedLength,
192
+ >(
193
+ xs: Xs,
194
+ length: Length,
195
+ ): xs is FixedLengthArray<Length, Xs[number]> & Xs => xs.length === length;
@@ -0,0 +1,169 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type MaxLengthArray,
5
+ type MinLengthArray,
6
+ } from 'ts-type-forge';
7
+ import { expectType } from '../../expect-type.mjs';
8
+ import {
9
+ isBoundedLengthArray,
10
+ isFixedLengthArray,
11
+ isMaxLengthArray,
12
+ isMinLengthArray,
13
+ } from './array-utils-length-bounded-array-guard.mjs';
14
+
15
+ describe(isMinLengthArray, () => {
16
+ test('should return true when the array is long enough', () => {
17
+ assert.isTrue(isMinLengthArray([1, 2, 3], 3));
18
+
19
+ assert.isTrue(isMinLengthArray([1, 2, 3], 2));
20
+
21
+ assert.isTrue(isMinLengthArray([], 0));
22
+ });
23
+
24
+ test('should return false when the array is too short', () => {
25
+ assert.isFalse(isMinLengthArray([1, 2], 3));
26
+
27
+ assert.isFalse(isMinLengthArray([], 1));
28
+ });
29
+
30
+ test('should act as a type guard', () => {
31
+ const value: readonly number[] = [0, 1, 2, 3] as const;
32
+
33
+ if (isMinLengthArray(value, 3)) {
34
+ expectType<typeof value, MinLengthArray<3, number>>('<=');
35
+
36
+ expectType<typeof value, MinLengthArray<1, number>>('<='); // 3 >= 1
37
+
38
+ expectType<typeof value, MinLengthArray<5, number>>('!<=');
39
+
40
+ // indexed access below the minimum length does not include `undefined`
41
+ // (even under `noUncheckedIndexedAccess`)
42
+ const _first = value[0];
43
+
44
+ expectType<typeof _first, number>('=');
45
+
46
+ const _third = value[2];
47
+
48
+ expectType<typeof _third, number>('=');
49
+
50
+ const _fourth = value[3];
51
+
52
+ expectType<typeof _fourth, number | undefined>('=');
53
+
54
+ expect(value.length).toBeGreaterThanOrEqual(3);
55
+ }
56
+ });
57
+
58
+ test('should preserve the original array type', () => {
59
+ const tuple = [1, 2, 3] as const;
60
+
61
+ if (isMinLengthArray(tuple, 2)) {
62
+ expectType<typeof tuple, readonly [1, 2, 3]>('<=');
63
+
64
+ expectType<typeof tuple, MinLengthArray<2, 1 | 2 | 3>>('<=');
65
+ }
66
+ });
67
+ });
68
+
69
+ describe(isMaxLengthArray, () => {
70
+ test('should return true when the array is short enough', () => {
71
+ assert.isTrue(isMaxLengthArray([1, 2, 3], 3));
72
+
73
+ assert.isTrue(isMaxLengthArray([1, 2, 3], 4));
74
+
75
+ assert.isTrue(isMaxLengthArray([], 0));
76
+ });
77
+
78
+ test('should return false when the array is too long', () => {
79
+ assert.isFalse(isMaxLengthArray([1, 2, 3], 2));
80
+
81
+ assert.isFalse(isMaxLengthArray([1], 0));
82
+ });
83
+
84
+ test('should act as a type guard', () => {
85
+ const value: readonly number[] = [0, 1, 2] as const;
86
+
87
+ if (isMaxLengthArray(value, 8)) {
88
+ expectType<typeof value, MaxLengthArray<8, number>>('<=');
89
+
90
+ expectType<typeof value, MaxLengthArray<16, number>>('<='); // 8 <= 16
91
+
92
+ expectType<typeof value, MaxLengthArray<2, number>>('!<=');
93
+
94
+ expect(value.length).toBeLessThanOrEqual(8);
95
+ }
96
+ });
97
+ });
98
+
99
+ describe(isBoundedLengthArray, () => {
100
+ test('should return true when the length is within the range', () => {
101
+ assert.isTrue(isBoundedLengthArray([1, 2, 3], 1, 5));
102
+
103
+ assert.isTrue(isBoundedLengthArray([1], 1, 5));
104
+
105
+ assert.isTrue(isBoundedLengthArray([1, 2, 3, 4, 5], 1, 5));
106
+ });
107
+
108
+ test('should return false when the length is out of the range', () => {
109
+ assert.isFalse(isBoundedLengthArray([], 1, 5));
110
+
111
+ assert.isFalse(isBoundedLengthArray([1, 2, 3, 4, 5, 6], 1, 5));
112
+ });
113
+
114
+ test('should act as a type guard', () => {
115
+ const value: readonly number[] = [1, 2, 3] as const;
116
+
117
+ if (isBoundedLengthArray(value, 1, 5)) {
118
+ expectType<typeof value, BoundedLengthArray<1, 5, number>>('<=');
119
+
120
+ expectType<typeof value, BoundedLengthArray<0, 100, number>>('<=');
121
+
122
+ expectType<typeof value, MinLengthArray<1, number>>('<=');
123
+
124
+ expectType<typeof value, MaxLengthArray<5, number>>('<=');
125
+
126
+ expectType<typeof value, BoundedLengthArray<2, 5, number>>('!<=');
127
+ }
128
+ });
129
+ });
130
+
131
+ describe(isFixedLengthArray, () => {
132
+ test('should return true when the length matches exactly', () => {
133
+ assert.isTrue(isFixedLengthArray([1, 2, 3], 3));
134
+
135
+ assert.isTrue(isFixedLengthArray([], 0));
136
+ });
137
+
138
+ test('should return false when the length does not match', () => {
139
+ assert.isFalse(isFixedLengthArray([1, 2, 3], 2));
140
+
141
+ assert.isFalse(isFixedLengthArray([1, 2, 3], 4));
142
+ });
143
+
144
+ test('should act as a type guard', () => {
145
+ const value: readonly number[] = [255, 128, 0] as const;
146
+
147
+ if (isFixedLengthArray(value, 3)) {
148
+ expectType<typeof value, FixedLengthArray<3, number>>('<=');
149
+
150
+ expectType<typeof value, MaxLengthArray<5, number>>('<='); // 3 <= 5
151
+
152
+ expectType<typeof value, MinLengthArray<1, number>>('<='); // 3 >= 1
153
+
154
+ expectType<typeof value, FixedLengthArray<4, number>>('!<=');
155
+
156
+ // `length` is narrowed to the literal and in-range indexed access does
157
+ // not include `undefined` (even under `noUncheckedIndexedAccess`)
158
+ const _len = value.length;
159
+
160
+ expectType<typeof _len, 3>('=');
161
+
162
+ const _second = value[1];
163
+
164
+ expectType<typeof _second, number>('=');
165
+
166
+ expect(value).toHaveLength(3);
167
+ }
168
+ });
169
+ });