ts-data-forge 12.0.0 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import {
3
3
  type FixedLengthString,
4
4
  type MaxLengthString,
5
5
  type MinLengthString,
6
+ type NonEmptyString,
6
7
  type SupportedLength,
7
8
  } from 'ts-type-forge';
8
9
 
@@ -200,6 +201,9 @@ export namespace Str {
200
201
  * length is verified at runtime, and the original string type (e.g. literal
201
202
  * types) is preserved via intersection.
202
203
  *
204
+ * Supports a curried overload: calling it with only `minLength` returns a
205
+ * function that casts a string, which is convenient for `pipe`/`map`.
206
+ *
203
207
  * @example
204
208
  *
205
209
  * ```ts
@@ -209,6 +213,11 @@ export namespace Str {
209
213
  *
210
214
  * assert.isTrue(nonEmpty.length >= 12);
211
215
  *
216
+ * // curried version
217
+ * const asPassword = Str.asMinLengthString(12);
218
+ *
219
+ * assert.strictEqual(asPassword('another-secret'), 'another-secret');
220
+ *
212
221
  * assert.throws(() => Str.asMinLengthString('short', 12)); // length 5 < 12
213
222
  * ```
214
223
  *
@@ -219,21 +228,41 @@ export namespace Str {
219
228
  * @returns `s` typed as `MinLengthString<MinLength> & S`.
220
229
  * @throws {TypeError} If `s.length < minLength`.
221
230
  */
222
- export const asMinLengthString = <
231
+ export function asMinLengthString<
223
232
  S extends string,
224
233
  MinLength extends SupportedLength,
225
- >(
226
- s: S,
234
+ >(s: S, minLength: MinLength): MinLengthString<MinLength> & S;
235
+
236
+ // Curried version
237
+ export function asMinLengthString<MinLength extends SupportedLength>(
227
238
  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
- }
239
+ ): <S extends string>(s: S) => MinLengthString<MinLength> & S;
234
240
 
235
- return s;
236
- };
241
+ export function asMinLengthString(
242
+ ...args:
243
+ | readonly [s: string, minLength: SupportedLength]
244
+ | readonly [minLength: SupportedLength]
245
+ ): string | ((s: string) => string) {
246
+ switch (args.length) {
247
+ case 2: {
248
+ const [s, minLength] = args;
249
+
250
+ if (!isMinLengthString(s, minLength)) {
251
+ throw new TypeError(
252
+ `Expected a string of length >= ${minLength}, got a string of length ${s.length}`,
253
+ );
254
+ }
255
+
256
+ return s;
257
+ }
258
+
259
+ case 1: {
260
+ const [minLength] = args;
261
+
262
+ return (s) => asMinLengthString(s, minLength);
263
+ }
264
+ }
265
+ }
237
266
 
238
267
  /**
239
268
  * Casts a string to `MaxLengthString<MaxLength>` after checking that it has
@@ -243,6 +272,9 @@ export namespace Str {
243
272
  * length is verified at runtime, and the original string type (e.g. literal
244
273
  * types) is preserved via intersection.
245
274
  *
275
+ * Supports a curried overload: calling it with only `maxLength` returns a
276
+ * function that casts a string, which is convenient for `pipe`/`map`.
277
+ *
246
278
  * @example
247
279
  *
248
280
  * ```ts
@@ -252,6 +284,11 @@ export namespace Str {
252
284
  *
253
285
  * assert.isTrue(relaxed.length <= 32);
254
286
  *
287
+ * // curried version
288
+ * const asUserName = Str.asMaxLengthString(32);
289
+ *
290
+ * assert.strictEqual(asUserName('another-user'), 'another-user');
291
+ *
255
292
  * assert.throws(() => Str.asMaxLengthString('noshiro', 3)); // length 7 > 3
256
293
  * ```
257
294
  *
@@ -262,21 +299,41 @@ export namespace Str {
262
299
  * @returns `s` typed as `MaxLengthString<MaxLength> & S`.
263
300
  * @throws {TypeError} If `s.length > maxLength`.
264
301
  */
265
- export const asMaxLengthString = <
302
+ export function asMaxLengthString<
266
303
  S extends string,
267
304
  MaxLength extends SupportedLength,
268
- >(
269
- s: S,
305
+ >(s: S, maxLength: MaxLength): MaxLengthString<MaxLength> & S;
306
+
307
+ // Curried version
308
+ export function asMaxLengthString<MaxLength extends SupportedLength>(
270
309
  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
- }
310
+ ): <S extends string>(s: S) => MaxLengthString<MaxLength> & S;
277
311
 
278
- return s;
279
- };
312
+ export function asMaxLengthString(
313
+ ...args:
314
+ | readonly [s: string, maxLength: SupportedLength]
315
+ | readonly [maxLength: SupportedLength]
316
+ ): string | ((s: string) => string) {
317
+ switch (args.length) {
318
+ case 2: {
319
+ const [s, maxLength] = args;
320
+
321
+ if (!isMaxLengthString(s, maxLength)) {
322
+ throw new TypeError(
323
+ `Expected a string of length <= ${maxLength}, got a string of length ${s.length}`,
324
+ );
325
+ }
326
+
327
+ return s;
328
+ }
329
+
330
+ case 1: {
331
+ const [maxLength] = args;
332
+
333
+ return (s) => asMaxLengthString(s, maxLength);
334
+ }
335
+ }
336
+ }
280
337
 
281
338
  /**
282
339
  * Casts a string to `BoundedLengthString<MinLength, MaxLength>` after
@@ -287,6 +344,10 @@ export namespace Str {
287
344
  * the length is verified at runtime, and the original string type (e.g.
288
345
  * literal types) is preserved via intersection.
289
346
  *
347
+ * Supports a curried overload: calling it with only `minLength` and
348
+ * `maxLength` returns a function that casts a string, which is convenient for
349
+ * `pipe`/`map`.
350
+ *
290
351
  * @example
291
352
  *
292
353
  * ```ts
@@ -296,6 +357,11 @@ export namespace Str {
296
357
  *
297
358
  * assert.isTrue(relaxed.length >= 8 && relaxed.length <= 16);
298
359
  *
360
+ * // curried version
361
+ * const asUserId = Str.asBoundedLengthString(8, 16);
362
+ *
363
+ * assert.strictEqual(asUserId('user-87654321'), 'user-87654321');
364
+ *
299
365
  * assert.throws(() => Str.asBoundedLengthString('user', 8, 16)); // length 4 < 8
300
366
  * ```
301
367
  *
@@ -308,7 +374,7 @@ export namespace Str {
308
374
  * @returns `s` typed as `BoundedLengthString<MinLength, MaxLength> & S`.
309
375
  * @throws {TypeError} If `s.length < minLength` or `s.length > maxLength`.
310
376
  */
311
- export const asBoundedLengthString = <
377
+ export function asBoundedLengthString<
312
378
  S extends string,
313
379
  MinLength extends SupportedLength,
314
380
  MaxLength extends SupportedLength,
@@ -316,15 +382,46 @@ export namespace Str {
316
382
  s: S,
317
383
  minLength: MinLength,
318
384
  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
- }
385
+ ): BoundedLengthString<MinLength, MaxLength> & S;
325
386
 
326
- return s;
327
- };
387
+ // Curried version
388
+ export function asBoundedLengthString<
389
+ MinLength extends SupportedLength,
390
+ MaxLength extends SupportedLength,
391
+ >(
392
+ minLength: MinLength,
393
+ maxLength: MaxLength,
394
+ ): <S extends string>(s: S) => BoundedLengthString<MinLength, MaxLength> & S;
395
+
396
+ export function asBoundedLengthString(
397
+ ...args:
398
+ | readonly [
399
+ s: string,
400
+ minLength: SupportedLength,
401
+ maxLength: SupportedLength,
402
+ ]
403
+ | readonly [minLength: SupportedLength, maxLength: SupportedLength]
404
+ ): string | ((s: string) => string) {
405
+ switch (args.length) {
406
+ case 3: {
407
+ const [s, minLength, maxLength] = args;
408
+
409
+ if (!isBoundedLengthString(s, minLength, maxLength)) {
410
+ throw new TypeError(
411
+ `Expected a string of length in [${minLength}, ${maxLength}], got a string of length ${s.length}`,
412
+ );
413
+ }
414
+
415
+ return s;
416
+ }
417
+
418
+ case 2: {
419
+ const [minLength, maxLength] = args;
420
+
421
+ return (s) => asBoundedLengthString(s, minLength, maxLength);
422
+ }
423
+ }
424
+ }
328
425
 
329
426
  /**
330
427
  * Casts a string to `FixedLengthString<Length>` after checking that it has
@@ -334,6 +431,9 @@ export namespace Str {
334
431
  * length is verified at runtime, and the original string type (e.g. literal
335
432
  * types) is preserved via intersection.
336
433
  *
434
+ * Supports a curried overload: calling it with only `length` returns a
435
+ * function that casts a string, which is convenient for `pipe`/`map`.
436
+ *
337
437
  * @example
338
438
  *
339
439
  * ```ts
@@ -343,6 +443,11 @@ export namespace Str {
343
443
  *
344
444
  * assert.isTrue(atMost5.length === 2);
345
445
  *
446
+ * // curried version
447
+ * const asCountryCode = Str.asFixedLengthString(2);
448
+ *
449
+ * assert.strictEqual(asCountryCode('US'), 'US');
450
+ *
346
451
  * assert.throws(() => Str.asFixedLengthString('JP', 3)); // length 2 !== 3
347
452
  * ```
348
453
  *
@@ -353,17 +458,74 @@ export namespace Str {
353
458
  * @returns `s` typed as `FixedLengthString<Length> & S`.
354
459
  * @throws {TypeError} If `s.length !== length`.
355
460
  */
356
- export const asFixedLengthString = <
461
+ export function asFixedLengthString<
357
462
  S extends string,
358
463
  Length extends SupportedLength,
359
- >(
360
- s: S,
464
+ >(s: S, length: Length): FixedLengthString<Length> & S;
465
+
466
+ // Curried version
467
+ export function asFixedLengthString<Length extends SupportedLength>(
361
468
  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
- );
469
+ ): <S extends string>(s: S) => FixedLengthString<Length> & S;
470
+
471
+ export function asFixedLengthString(
472
+ ...args:
473
+ | readonly [s: string, length: SupportedLength]
474
+ | readonly [length: SupportedLength]
475
+ ): string | ((s: string) => string) {
476
+ switch (args.length) {
477
+ case 2: {
478
+ const [s, length] = args;
479
+
480
+ if (!isFixedLengthString(s, length)) {
481
+ throw new TypeError(
482
+ `Expected a string of length ${length}, got a string of length ${s.length}`,
483
+ );
484
+ }
485
+
486
+ return s;
487
+ }
488
+
489
+ case 1: {
490
+ const [length] = args;
491
+
492
+ return (s) => asFixedLengthString(s, length);
493
+ }
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Casts a string to `NonEmptyString` after checking that it has at least one
499
+ * character.
500
+ *
501
+ * `NonEmptyString` is an alias of `MinLengthString<1>`, so this is the
502
+ * length-1 specialization of {@link asMinLengthString}. Use this instead of
503
+ * writing `s as unknown as NonEmptyString`: the length is verified at
504
+ * runtime, and the original string type (e.g. literal types) is preserved
505
+ * via intersection.
506
+ *
507
+ * @example
508
+ *
509
+ * ```ts
510
+ * const name = Str.asNonEmptyString('noshiro');
511
+ *
512
+ * const nonEmpty: NonEmptyString = name; // OK
513
+ *
514
+ * assert.isTrue(nonEmpty.length >= 1);
515
+ *
516
+ * assert.throws(() => Str.asNonEmptyString('')); // length 0 < 1
517
+ * ```
518
+ *
519
+ * @template S - The input string type (literal types are preserved).
520
+ * @param s - The string to cast.
521
+ * @returns `s` typed as `NonEmptyString & S`.
522
+ * @throws {TypeError} If `s` is empty.
523
+ */
524
+ export const asNonEmptyString = <S extends string>(
525
+ s: S,
526
+ ): NonEmptyString & S => {
527
+ if (!isMinLengthString(s, 1)) {
528
+ throw new TypeError('Expected a non-empty string, got an empty string');
367
529
  }
368
530
 
369
531
  return s;
@@ -213,3 +213,79 @@ describe('Str length-bounded casts', () => {
213
213
  assert.throws(() => Str.asFixedLengthString('JP', 3), TypeError);
214
214
  });
215
215
  });
216
+
217
+ describe('Str length-bounded casts (curried)', () => {
218
+ test('asMinLengthString returns the value and narrows its type', () => {
219
+ const result = Str.asMinLengthString(3)('very-secret');
220
+
221
+ expectType<typeof result, MinLengthString<3>>('<=');
222
+
223
+ assert.strictEqual(result, 'very-secret');
224
+ });
225
+
226
+ test('asMinLengthString throws when too short', () => {
227
+ assert.throws(() => Str.asMinLengthString(3)('hi'), TypeError);
228
+ });
229
+
230
+ test('asMaxLengthString returns the value and narrows its type', () => {
231
+ const result = Str.asMaxLengthString(32)('noshiro');
232
+
233
+ expectType<typeof result, MaxLengthString<32>>('<=');
234
+
235
+ assert.strictEqual(result, 'noshiro');
236
+ });
237
+
238
+ test('asMaxLengthString throws when too long', () => {
239
+ assert.throws(() => Str.asMaxLengthString(3)('noshiro'), TypeError);
240
+ });
241
+
242
+ test('asBoundedLengthString returns the value and narrows its type', () => {
243
+ const result = Str.asBoundedLengthString(8, 16)('user-12345678');
244
+
245
+ expectType<typeof result, BoundedLengthString<8, 16>>('<=');
246
+
247
+ assert.strictEqual(result, 'user-12345678');
248
+ });
249
+
250
+ test('asBoundedLengthString throws when out of range', () => {
251
+ assert.throws(() => Str.asBoundedLengthString(8, 16)('user'), TypeError);
252
+ });
253
+
254
+ test('asFixedLengthString returns the value and narrows its type', () => {
255
+ const result = Str.asFixedLengthString(2)('JP');
256
+
257
+ expectType<typeof result, FixedLengthString<2>>('<=');
258
+
259
+ assert.strictEqual(result, 'JP');
260
+ });
261
+
262
+ test('asFixedLengthString throws when the length differs', () => {
263
+ assert.throws(() => Str.asFixedLengthString(3)('JP'), TypeError);
264
+ });
265
+ });
266
+
267
+ describe(Str.asNonEmptyString, () => {
268
+ test('returns the value and narrows its type', () => {
269
+ const result = Str.asNonEmptyString('noshiro');
270
+
271
+ expectType<typeof result, NonEmptyString>('<=');
272
+
273
+ expectType<typeof result, MinLengthString<1>>('<=');
274
+
275
+ assert.strictEqual(result, 'noshiro');
276
+ });
277
+
278
+ test('throws when the string is empty', () => {
279
+ assert.throws(() => Str.asNonEmptyString(''), TypeError);
280
+ });
281
+
282
+ test('preserves literal string types', () => {
283
+ const result = Str.asNonEmptyString('hello');
284
+
285
+ expectType<typeof result, 'hello'>('<=');
286
+
287
+ expectType<typeof result, NonEmptyString>('<=');
288
+
289
+ assert.strictEqual(result, 'hello');
290
+ });
291
+ });