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,371 @@
1
+ import {
2
+ type BoundedLengthString,
3
+ type FixedLengthString,
4
+ type MaxLengthString,
5
+ type MinLengthString,
6
+ type SupportedLength,
7
+ } from 'ts-type-forge';
8
+
9
+ /**
10
+ * Namespace providing type guards and runtime-checked casts for the branded
11
+ * length-constrained string types (`MinLengthString`, `MaxLengthString`,
12
+ * `BoundedLengthString`, `FixedLengthString`).
13
+ *
14
+ * Lengths are measured in UTF-16 code units (the same unit as
15
+ * `String.prototype.length`), so characters outside the Basic Multilingual
16
+ * Plane (e.g. emoji) count as 2.
17
+ */
18
+ export namespace Str {
19
+ /**
20
+ * Type guard that checks if a string has at least `minLength` characters.
21
+ *
22
+ * **Type Narrowing Behavior:**
23
+ *
24
+ * - Narrows the input to `MinLengthString<MinLength>` while preserving the
25
+ * original string type (e.g. literal types) via intersection.
26
+ * - The result participates in the length-constraint subtyping relation: for
27
+ * example, a value narrowed to `MinLengthString<3>` is assignable to
28
+ * `MinLengthString<1>` (= `NonEmptyString`).
29
+ *
30
+ * @example
31
+ *
32
+ * ```ts
33
+ * const input: string = 'very-secret-password';
34
+ *
35
+ * assert.isTrue(Str.isMinLengthString(input, 12));
36
+ *
37
+ * assert.isFalse(Str.isMinLengthString('short', 12));
38
+ *
39
+ * if (Str.isMinLengthString(input, 12)) {
40
+ * const nonEmpty: NonEmptyString = input; // OK (12 >= 1)
41
+ *
42
+ * assert.isTrue(nonEmpty.length >= 12);
43
+ * }
44
+ * ```
45
+ *
46
+ * @template S - The input string type (literal types are preserved).
47
+ * @template MinLength - The minimum number of characters (inclusive).
48
+ * @param s - The string to check.
49
+ * @param minLength - The minimum number of characters (inclusive).
50
+ * @returns `true` if `s.length >= minLength`, `false` otherwise. When `true`,
51
+ * TypeScript narrows `s` to `MinLengthString<MinLength> & S`.
52
+ */
53
+ export const isMinLengthString = <
54
+ S extends string,
55
+ MinLength extends SupportedLength,
56
+ >(
57
+ s: S,
58
+ minLength: MinLength,
59
+ ): s is MinLengthString<MinLength> & S => s.length >= minLength;
60
+
61
+ /**
62
+ * Type guard that checks if a string has at most `maxLength` characters.
63
+ *
64
+ * **Type Narrowing Behavior:**
65
+ *
66
+ * - Narrows the input to `MaxLengthString<MaxLength>` while preserving the
67
+ * original string type (e.g. literal types) via intersection.
68
+ * - The result participates in the length-constraint subtyping relation: for
69
+ * example, a value narrowed to `MaxLengthString<16>` is assignable to
70
+ * `MaxLengthString<32>`.
71
+ *
72
+ * @example
73
+ *
74
+ * ```ts
75
+ * const input: string = 'noshiro';
76
+ *
77
+ * assert.isTrue(Str.isMaxLengthString(input, 32));
78
+ *
79
+ * assert.isFalse(Str.isMaxLengthString(input, 3));
80
+ *
81
+ * if (Str.isMaxLengthString(input, 32)) {
82
+ * const relaxed: MaxLengthString<64> = input; // OK (32 <= 64)
83
+ *
84
+ * assert.isTrue(relaxed.length <= 32);
85
+ * }
86
+ * ```
87
+ *
88
+ * @template S - The input string type (literal types are preserved).
89
+ * @template MaxLength - The maximum number of characters (inclusive).
90
+ * @param s - The string to check.
91
+ * @param maxLength - The maximum number of characters (inclusive).
92
+ * @returns `true` if `s.length <= maxLength`, `false` otherwise. When `true`,
93
+ * TypeScript narrows `s` to `MaxLengthString<MaxLength> & S`.
94
+ */
95
+ export const isMaxLengthString = <
96
+ S extends string,
97
+ MaxLength extends SupportedLength,
98
+ >(
99
+ s: S,
100
+ maxLength: MaxLength,
101
+ ): s is MaxLengthString<MaxLength> & S => s.length <= maxLength;
102
+
103
+ /**
104
+ * Type guard that checks if a string's length is within the inclusive range
105
+ * `[minLength, maxLength]`.
106
+ *
107
+ * **Type Narrowing Behavior:**
108
+ *
109
+ * - Narrows the input to `BoundedLengthString<MinLength, MaxLength>` while
110
+ * preserving the original string type (e.g. literal types) via intersection.
111
+ * - Since `BoundedLengthString` is the intersection of `MinLengthString` and
112
+ * `MaxLengthString`, the narrowed value is assignable to both, and both
113
+ * bounds can be weakened independently.
114
+ *
115
+ * @example
116
+ *
117
+ * ```ts
118
+ * const input: string = 'user-12345678';
119
+ *
120
+ * assert.isTrue(Str.isBoundedLengthString(input, 8, 16));
121
+ *
122
+ * assert.isFalse(Str.isBoundedLengthString('user', 8, 16));
123
+ *
124
+ * if (Str.isBoundedLengthString(input, 8, 16)) {
125
+ * const userId: BoundedLengthString<1, 255> = input; // OK ([8, 16] ⊆ [1, 255])
126
+ *
127
+ * assert.isTrue(userId.length >= 8 && userId.length <= 16);
128
+ * }
129
+ * ```
130
+ *
131
+ * @template S - The input string type (literal types are preserved).
132
+ * @template MinLength - The minimum number of characters (inclusive).
133
+ * @template MaxLength - The maximum number of characters (inclusive).
134
+ * @param s - The string to check.
135
+ * @param minLength - The minimum number of characters (inclusive).
136
+ * @param maxLength - The maximum number of characters (inclusive).
137
+ * @returns `true` if `minLength <= s.length && s.length <= maxLength`, `false`
138
+ * otherwise. When `true`, TypeScript narrows `s` to
139
+ * `BoundedLengthString<MinLength, MaxLength> & S`.
140
+ */
141
+ export const isBoundedLengthString = <
142
+ S extends string,
143
+ MinLength extends SupportedLength,
144
+ MaxLength extends SupportedLength,
145
+ >(
146
+ s: S,
147
+ minLength: MinLength,
148
+ maxLength: MaxLength,
149
+ ): s is BoundedLengthString<MinLength, MaxLength> & S =>
150
+ s.length >= minLength && s.length <= maxLength;
151
+
152
+ /**
153
+ * Type guard that checks if a string has exactly `length` characters.
154
+ *
155
+ * **Type Narrowing Behavior:**
156
+ *
157
+ * - Narrows the input to `FixedLengthString<Length>` while preserving the
158
+ * original string type (e.g. literal types) via intersection.
159
+ * - Since `FixedLengthString<Length>` is defined as
160
+ * `BoundedLengthString<Length, Length>`, the narrowed value is assignable to
161
+ * any `MinLengthString<N>` with `N <= Length` and any `MaxLengthString<N>`
162
+ * with `N >= Length`.
163
+ *
164
+ * @example
165
+ *
166
+ * ```ts
167
+ * const input: string = 'JP';
168
+ *
169
+ * assert.isTrue(Str.isFixedLengthString(input, 2));
170
+ *
171
+ * assert.isFalse(Str.isFixedLengthString(input, 3));
172
+ *
173
+ * if (Str.isFixedLengthString(input, 2)) {
174
+ * const atMost5: MaxLengthString<5> = input; // OK (2 <= 5)
175
+ *
176
+ * assert.isTrue(atMost5.length === 2);
177
+ * }
178
+ * ```
179
+ *
180
+ * @template S - The input string type (literal types are preserved).
181
+ * @template Length - The exact number of characters.
182
+ * @param s - The string to check.
183
+ * @param length - The exact number of characters.
184
+ * @returns `true` if `s.length === length`, `false` otherwise. When `true`,
185
+ * TypeScript narrows `s` to `FixedLengthString<Length> & S`.
186
+ */
187
+ export const isFixedLengthString = <
188
+ S extends string,
189
+ Length extends SupportedLength,
190
+ >(
191
+ s: S,
192
+ length: Length,
193
+ ): s is FixedLengthString<Length> & S => s.length === length;
194
+
195
+ /**
196
+ * Casts a string to `MinLengthString<MinLength>` after checking that it has
197
+ * at least `minLength` characters.
198
+ *
199
+ * Use this instead of writing `s as unknown as MinLengthString<N>`: the
200
+ * length is verified at runtime, and the original string type (e.g. literal
201
+ * types) is preserved via intersection.
202
+ *
203
+ * @example
204
+ *
205
+ * ```ts
206
+ * const password = Str.asMinLengthString('very-secret-password', 12);
207
+ *
208
+ * const nonEmpty: NonEmptyString = password; // OK (>= 1)
209
+ *
210
+ * assert.isTrue(nonEmpty.length >= 12);
211
+ *
212
+ * assert.throws(() => Str.asMinLengthString('short', 12)); // length 5 < 12
213
+ * ```
214
+ *
215
+ * @template S - The input string type (literal types are preserved).
216
+ * @template MinLength - The minimum number of characters (inclusive).
217
+ * @param s - The string to cast.
218
+ * @param minLength - The minimum number of characters (inclusive).
219
+ * @returns `s` typed as `MinLengthString<MinLength> & S`.
220
+ * @throws {TypeError} If `s.length < minLength`.
221
+ */
222
+ export const asMinLengthString = <
223
+ S extends string,
224
+ MinLength extends SupportedLength,
225
+ >(
226
+ s: S,
227
+ minLength: MinLength,
228
+ ): MinLengthString<MinLength> & S => {
229
+ if (!isMinLengthString(s, minLength)) {
230
+ throw new TypeError(
231
+ `Expected a string of length >= ${minLength}, got a string of length ${s.length}`,
232
+ );
233
+ }
234
+
235
+ return s;
236
+ };
237
+
238
+ /**
239
+ * Casts a string to `MaxLengthString<MaxLength>` after checking that it has
240
+ * at most `maxLength` characters.
241
+ *
242
+ * Use this instead of writing `s as unknown as MaxLengthString<N>`: the
243
+ * length is verified at runtime, and the original string type (e.g. literal
244
+ * types) is preserved via intersection.
245
+ *
246
+ * @example
247
+ *
248
+ * ```ts
249
+ * const userName = Str.asMaxLengthString('noshiro', 32);
250
+ *
251
+ * const relaxed: MaxLengthString<64> = userName; // OK (32 <= 64)
252
+ *
253
+ * assert.isTrue(relaxed.length <= 32);
254
+ *
255
+ * assert.throws(() => Str.asMaxLengthString('noshiro', 3)); // length 7 > 3
256
+ * ```
257
+ *
258
+ * @template S - The input string type (literal types are preserved).
259
+ * @template MaxLength - The maximum number of characters (inclusive).
260
+ * @param s - The string to cast.
261
+ * @param maxLength - The maximum number of characters (inclusive).
262
+ * @returns `s` typed as `MaxLengthString<MaxLength> & S`.
263
+ * @throws {TypeError} If `s.length > maxLength`.
264
+ */
265
+ export const asMaxLengthString = <
266
+ S extends string,
267
+ MaxLength extends SupportedLength,
268
+ >(
269
+ s: S,
270
+ maxLength: MaxLength,
271
+ ): MaxLengthString<MaxLength> & S => {
272
+ if (!isMaxLengthString(s, maxLength)) {
273
+ throw new TypeError(
274
+ `Expected a string of length <= ${maxLength}, got a string of length ${s.length}`,
275
+ );
276
+ }
277
+
278
+ return s;
279
+ };
280
+
281
+ /**
282
+ * Casts a string to `BoundedLengthString<MinLength, MaxLength>` after
283
+ * checking that its length is within the inclusive range
284
+ * `[minLength, maxLength]`.
285
+ *
286
+ * Use this instead of writing `s as unknown as BoundedLengthString<M, N>`:
287
+ * the length is verified at runtime, and the original string type (e.g.
288
+ * literal types) is preserved via intersection.
289
+ *
290
+ * @example
291
+ *
292
+ * ```ts
293
+ * const userId = Str.asBoundedLengthString('user-12345678', 8, 16);
294
+ *
295
+ * const relaxed: BoundedLengthString<1, 255> = userId; // OK ([8, 16] ⊆ [1, 255])
296
+ *
297
+ * assert.isTrue(relaxed.length >= 8 && relaxed.length <= 16);
298
+ *
299
+ * assert.throws(() => Str.asBoundedLengthString('user', 8, 16)); // length 4 < 8
300
+ * ```
301
+ *
302
+ * @template S - The input string type (literal types are preserved).
303
+ * @template MinLength - The minimum number of characters (inclusive).
304
+ * @template MaxLength - The maximum number of characters (inclusive).
305
+ * @param s - The string to cast.
306
+ * @param minLength - The minimum number of characters (inclusive).
307
+ * @param maxLength - The maximum number of characters (inclusive).
308
+ * @returns `s` typed as `BoundedLengthString<MinLength, MaxLength> & S`.
309
+ * @throws {TypeError} If `s.length < minLength` or `s.length > maxLength`.
310
+ */
311
+ export const asBoundedLengthString = <
312
+ S extends string,
313
+ MinLength extends SupportedLength,
314
+ MaxLength extends SupportedLength,
315
+ >(
316
+ s: S,
317
+ minLength: MinLength,
318
+ maxLength: MaxLength,
319
+ ): BoundedLengthString<MinLength, MaxLength> & S => {
320
+ if (!isBoundedLengthString(s, minLength, maxLength)) {
321
+ throw new TypeError(
322
+ `Expected a string of length in [${minLength}, ${maxLength}], got a string of length ${s.length}`,
323
+ );
324
+ }
325
+
326
+ return s;
327
+ };
328
+
329
+ /**
330
+ * Casts a string to `FixedLengthString<Length>` after checking that it has
331
+ * exactly `length` characters.
332
+ *
333
+ * Use this instead of writing `s as unknown as FixedLengthString<N>`: the
334
+ * length is verified at runtime, and the original string type (e.g. literal
335
+ * types) is preserved via intersection.
336
+ *
337
+ * @example
338
+ *
339
+ * ```ts
340
+ * const countryCode = Str.asFixedLengthString('JP', 2);
341
+ *
342
+ * const atMost5: MaxLengthString<5> = countryCode; // OK (2 <= 5)
343
+ *
344
+ * assert.isTrue(atMost5.length === 2);
345
+ *
346
+ * assert.throws(() => Str.asFixedLengthString('JP', 3)); // length 2 !== 3
347
+ * ```
348
+ *
349
+ * @template S - The input string type (literal types are preserved).
350
+ * @template Length - The exact number of characters.
351
+ * @param s - The string to cast.
352
+ * @param length - The exact number of characters.
353
+ * @returns `s` typed as `FixedLengthString<Length> & S`.
354
+ * @throws {TypeError} If `s.length !== length`.
355
+ */
356
+ export const asFixedLengthString = <
357
+ S extends string,
358
+ Length extends SupportedLength,
359
+ >(
360
+ s: S,
361
+ length: Length,
362
+ ): FixedLengthString<Length> & S => {
363
+ if (!isFixedLengthString(s, length)) {
364
+ throw new TypeError(
365
+ `Expected a string of length ${length}, got a string of length ${s.length}`,
366
+ );
367
+ }
368
+
369
+ return s;
370
+ };
371
+ }
@@ -0,0 +1,215 @@
1
+ import {
2
+ type BoundedLengthString,
3
+ type FixedLengthString,
4
+ type MaxLengthString,
5
+ type MinLengthString,
6
+ type NonEmptyString,
7
+ } from 'ts-type-forge';
8
+ import { expectType } from '../expect-type.mjs';
9
+ import { Str } from './str.mjs';
10
+
11
+ describe(Str.isMinLengthString, () => {
12
+ test('should return true when the string is long enough', () => {
13
+ assert.isTrue(Str.isMinLengthString('hello', 3));
14
+
15
+ assert.isTrue(Str.isMinLengthString('hello', 5));
16
+
17
+ assert.isTrue(Str.isMinLengthString('', 0));
18
+
19
+ assert.isTrue(Str.isMinLengthString(' ', 1)); // Space counts
20
+ });
21
+
22
+ test('should return false when the string is too short', () => {
23
+ assert.isFalse(Str.isMinLengthString('hi', 3));
24
+
25
+ assert.isFalse(Str.isMinLengthString('', 1));
26
+ });
27
+
28
+ test('should measure length in UTF-16 code units', () => {
29
+ assert.isTrue(Str.isMinLengthString('🎉', 2)); // Surrogate pair
30
+
31
+ assert.isFalse(Str.isMinLengthString('🎉', 3));
32
+ });
33
+
34
+ test('should act as a type guard', () => {
35
+ const value: string = 'very-secret-password';
36
+
37
+ if (Str.isMinLengthString(value, 12)) {
38
+ expectType<typeof value, MinLengthString<12>>('<=');
39
+
40
+ expectType<typeof value, NonEmptyString>('<='); // 12 >= 1
41
+
42
+ expectType<typeof value, MinLengthString<16>>('!<=');
43
+
44
+ expect(value.length).toBeGreaterThanOrEqual(12);
45
+ }
46
+ });
47
+
48
+ test('should preserve literal string types', () => {
49
+ const literal = 'hello';
50
+
51
+ if (Str.isMinLengthString(literal, 3)) {
52
+ expectType<typeof literal, 'hello'>('<=');
53
+
54
+ expectType<typeof literal, MinLengthString<3>>('<=');
55
+ }
56
+ });
57
+ });
58
+
59
+ describe(Str.isMaxLengthString, () => {
60
+ test('should return true when the string is short enough', () => {
61
+ assert.isTrue(Str.isMaxLengthString('hello', 10));
62
+
63
+ assert.isTrue(Str.isMaxLengthString('hello', 5));
64
+
65
+ assert.isTrue(Str.isMaxLengthString('', 0));
66
+ });
67
+
68
+ test('should return false when the string is too long', () => {
69
+ assert.isFalse(Str.isMaxLengthString('hello', 3));
70
+
71
+ assert.isFalse(Str.isMaxLengthString(' ', 0));
72
+ });
73
+
74
+ test('should measure length in UTF-16 code units', () => {
75
+ assert.isFalse(Str.isMaxLengthString('🎉', 1)); // Surrogate pair
76
+
77
+ assert.isTrue(Str.isMaxLengthString('🎉', 2));
78
+ });
79
+
80
+ test('should act as a type guard', () => {
81
+ const value: string = 'noshiro';
82
+
83
+ if (Str.isMaxLengthString(value, 32)) {
84
+ expectType<typeof value, MaxLengthString<32>>('<=');
85
+
86
+ expectType<typeof value, MaxLengthString<64>>('<='); // 32 <= 64
87
+
88
+ expectType<typeof value, MaxLengthString<16>>('!<=');
89
+
90
+ expect(value.length).toBeLessThanOrEqual(32);
91
+ }
92
+ });
93
+ });
94
+
95
+ describe(Str.isBoundedLengthString, () => {
96
+ test('should return true when the length is within the range', () => {
97
+ assert.isTrue(Str.isBoundedLengthString('user-12345678', 8, 16));
98
+
99
+ assert.isTrue(Str.isBoundedLengthString('12345678', 8, 16)); // Lower bound (inclusive)
100
+
101
+ assert.isTrue(Str.isBoundedLengthString('1234567890123456', 8, 16)); // Upper bound (inclusive)
102
+
103
+ assert.isTrue(Str.isBoundedLengthString('', 0, 0));
104
+ });
105
+
106
+ test('should return false when the length is out of range', () => {
107
+ assert.isFalse(Str.isBoundedLengthString('user', 8, 16));
108
+
109
+ assert.isFalse(Str.isBoundedLengthString('12345678901234567', 8, 16));
110
+ });
111
+
112
+ test('should act as a type guard', () => {
113
+ const value: string = 'user-12345678';
114
+
115
+ if (Str.isBoundedLengthString(value, 8, 16)) {
116
+ expectType<typeof value, BoundedLengthString<8, 16>>('<=');
117
+
118
+ expectType<typeof value, BoundedLengthString<1, 255>>('<='); // [8, 16] ⊆ [1, 255]
119
+
120
+ expectType<typeof value, MinLengthString<8>>('<=');
121
+
122
+ expectType<typeof value, MaxLengthString<16>>('<=');
123
+
124
+ expectType<typeof value, NonEmptyString>('<='); // 8 >= 1
125
+
126
+ expectType<typeof value, BoundedLengthString<10, 16>>('!<=');
127
+ }
128
+ });
129
+ });
130
+
131
+ describe(Str.isFixedLengthString, () => {
132
+ test('should return true only for the exact length', () => {
133
+ assert.isTrue(Str.isFixedLengthString('JP', 2));
134
+
135
+ assert.isTrue(Str.isFixedLengthString('', 0));
136
+
137
+ assert.isFalse(Str.isFixedLengthString('JPN', 2));
138
+
139
+ assert.isFalse(Str.isFixedLengthString('J', 2));
140
+ });
141
+
142
+ test('should measure length in UTF-16 code units', () => {
143
+ assert.isTrue(Str.isFixedLengthString('🎉', 2)); // Surrogate pair
144
+
145
+ assert.isFalse(Str.isFixedLengthString('🎉', 1));
146
+ });
147
+
148
+ test('should act as a type guard', () => {
149
+ const value: string = 'JP';
150
+
151
+ if (Str.isFixedLengthString(value, 2)) {
152
+ expectType<typeof value, FixedLengthString<2>>('<=');
153
+
154
+ expectType<typeof value, BoundedLengthString<2, 2>>('<=');
155
+
156
+ expectType<typeof value, MaxLengthString<5>>('<='); // 2 <= 5
157
+
158
+ expectType<typeof value, MinLengthString<1>>('<='); // 2 >= 1
159
+
160
+ expectType<typeof value, NonEmptyString>('<=');
161
+
162
+ expectType<typeof value, FixedLengthString<3>>('!<=');
163
+ }
164
+ });
165
+ });
166
+
167
+ describe('Str length-bounded casts', () => {
168
+ test('asMinLengthString returns the value and narrows its type', () => {
169
+ const result = Str.asMinLengthString('very-secret', 3);
170
+
171
+ expectType<typeof result, MinLengthString<3>>('<=');
172
+
173
+ assert.strictEqual(result, 'very-secret');
174
+ });
175
+
176
+ test('asMinLengthString throws when too short', () => {
177
+ assert.throws(() => Str.asMinLengthString('hi', 3), TypeError);
178
+ });
179
+
180
+ test('asMaxLengthString returns the value and narrows its type', () => {
181
+ const result = Str.asMaxLengthString('noshiro', 32);
182
+
183
+ expectType<typeof result, MaxLengthString<32>>('<=');
184
+
185
+ assert.strictEqual(result, 'noshiro');
186
+ });
187
+
188
+ test('asMaxLengthString throws when too long', () => {
189
+ assert.throws(() => Str.asMaxLengthString('noshiro', 3), TypeError);
190
+ });
191
+
192
+ test('asBoundedLengthString returns the value and narrows its type', () => {
193
+ const result = Str.asBoundedLengthString('user-12345678', 8, 16);
194
+
195
+ expectType<typeof result, BoundedLengthString<8, 16>>('<=');
196
+
197
+ assert.strictEqual(result, 'user-12345678');
198
+ });
199
+
200
+ test('asBoundedLengthString throws when out of range', () => {
201
+ assert.throws(() => Str.asBoundedLengthString('user', 8, 16), TypeError);
202
+ });
203
+
204
+ test('asFixedLengthString returns the value and narrows its type', () => {
205
+ const result = Str.asFixedLengthString('JP', 2);
206
+
207
+ expectType<typeof result, FixedLengthString<2>>('<=');
208
+
209
+ assert.strictEqual(result, 'JP');
210
+ });
211
+
212
+ test('asFixedLengthString throws when the length differs', () => {
213
+ assert.throws(() => Str.asFixedLengthString('JP', 3), TypeError);
214
+ });
215
+ });