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.
- package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts +65 -5
- package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts.map +1 -1
- package/dist/array/impl/array-utils-length-bounded-array-cast.mjs +69 -27
- package/dist/array/impl/array-utils-length-bounded-array-cast.mjs.map +1 -1
- package/dist/array/impl/index.mjs +1 -1
- package/dist/string/str.d.mts +70 -5
- package/dist/string/str.d.mts.map +1 -1
- package/dist/string/str.mjs +72 -26
- package/dist/string/str.mjs.map +1 -1
- package/package.json +1 -1
- package/src/array/impl/array-utils-length-bounded-array-cast.mts +204 -39
- package/src/array/impl/array-utils-length-bounded-array-cast.test.mts +126 -0
- package/src/string/str.mts +201 -39
- package/src/string/str.test.mts +76 -0
package/dist/string/str.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"str.mjs","sources":["../../src/string/str.mts"],"sourcesContent":["import {\n type BoundedLengthString,\n type FixedLengthString,\n type MaxLengthString,\n type MinLengthString,\n type SupportedLength,\n} from 'ts-type-forge';\n\n/**\n * Namespace providing type guards and runtime-checked casts for the branded\n * length-constrained string types (`MinLengthString`, `MaxLengthString`,\n * `BoundedLengthString`, `FixedLengthString`).\n *\n * Lengths are measured in UTF-16 code units (the same unit as\n * `String.prototype.length`), so characters outside the Basic Multilingual\n * Plane (e.g. emoji) count as 2.\n */\nexport namespace Str {\n /**\n * Type guard that checks if a string has at least `minLength` characters.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `MinLengthString<MinLength>` while preserving the\n * original string type (e.g. literal types) via intersection.\n * - The result participates in the length-constraint subtyping relation: for\n * example, a value narrowed to `MinLengthString<3>` is assignable to\n * `MinLengthString<1>` (= `NonEmptyString`).\n *\n * @example\n *\n * ```ts\n * const input: string = 'very-secret-password';\n *\n * assert.isTrue(Str.isMinLengthString(input, 12));\n *\n * assert.isFalse(Str.isMinLengthString('short', 12));\n *\n * if (Str.isMinLengthString(input, 12)) {\n * const nonEmpty: NonEmptyString = input; // OK (12 >= 1)\n *\n * assert.isTrue(nonEmpty.length >= 12);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @param s - The string to check.\n * @param minLength - The minimum number of characters (inclusive).\n * @returns `true` if `s.length >= minLength`, `false` otherwise. When `true`,\n * TypeScript narrows `s` to `MinLengthString<MinLength> & S`.\n */\n export const isMinLengthString = <\n S extends string,\n MinLength extends SupportedLength,\n >(\n s: S,\n minLength: MinLength,\n ): s is MinLengthString<MinLength> & S => s.length >= minLength;\n\n /**\n * Type guard that checks if a string has at most `maxLength` characters.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `MaxLengthString<MaxLength>` while preserving the\n * original string type (e.g. literal types) via intersection.\n * - The result participates in the length-constraint subtyping relation: for\n * example, a value narrowed to `MaxLengthString<16>` is assignable to\n * `MaxLengthString<32>`.\n *\n * @example\n *\n * ```ts\n * const input: string = 'noshiro';\n *\n * assert.isTrue(Str.isMaxLengthString(input, 32));\n *\n * assert.isFalse(Str.isMaxLengthString(input, 3));\n *\n * if (Str.isMaxLengthString(input, 32)) {\n * const relaxed: MaxLengthString<64> = input; // OK (32 <= 64)\n *\n * assert.isTrue(relaxed.length <= 32);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to check.\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `true` if `s.length <= maxLength`, `false` otherwise. When `true`,\n * TypeScript narrows `s` to `MaxLengthString<MaxLength> & S`.\n */\n export const isMaxLengthString = <\n S extends string,\n MaxLength extends SupportedLength,\n >(\n s: S,\n maxLength: MaxLength,\n ): s is MaxLengthString<MaxLength> & S => s.length <= maxLength;\n\n /**\n * Type guard that checks if a string's length is within the inclusive range\n * `[minLength, maxLength]`.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `BoundedLengthString<MinLength, MaxLength>` while\n * preserving the original string type (e.g. literal types) via intersection.\n * - Since `BoundedLengthString` is the intersection of `MinLengthString` and\n * `MaxLengthString`, the narrowed value is assignable to both, and both\n * bounds can be weakened independently.\n *\n * @example\n *\n * ```ts\n * const input: string = 'user-12345678';\n *\n * assert.isTrue(Str.isBoundedLengthString(input, 8, 16));\n *\n * assert.isFalse(Str.isBoundedLengthString('user', 8, 16));\n *\n * if (Str.isBoundedLengthString(input, 8, 16)) {\n * const userId: BoundedLengthString<1, 255> = input; // OK ([8, 16] ⊆ [1, 255])\n *\n * assert.isTrue(userId.length >= 8 && userId.length <= 16);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to check.\n * @param minLength - The minimum number of characters (inclusive).\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `true` if `minLength <= s.length && s.length <= maxLength`, `false`\n * otherwise. When `true`, TypeScript narrows `s` to\n * `BoundedLengthString<MinLength, MaxLength> & S`.\n */\n export const isBoundedLengthString = <\n S extends string,\n MinLength extends SupportedLength,\n MaxLength extends SupportedLength,\n >(\n s: S,\n minLength: MinLength,\n maxLength: MaxLength,\n ): s is BoundedLengthString<MinLength, MaxLength> & S =>\n s.length >= minLength && s.length <= maxLength;\n\n /**\n * Type guard that checks if a string has exactly `length` characters.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `FixedLengthString<Length>` while preserving the\n * original string type (e.g. literal types) via intersection.\n * - Since `FixedLengthString<Length>` is defined as\n * `BoundedLengthString<Length, Length>`, the narrowed value is assignable to\n * any `MinLengthString<N>` with `N <= Length` and any `MaxLengthString<N>`\n * with `N >= Length`.\n *\n * @example\n *\n * ```ts\n * const input: string = 'JP';\n *\n * assert.isTrue(Str.isFixedLengthString(input, 2));\n *\n * assert.isFalse(Str.isFixedLengthString(input, 3));\n *\n * if (Str.isFixedLengthString(input, 2)) {\n * const atMost5: MaxLengthString<5> = input; // OK (2 <= 5)\n *\n * assert.isTrue(atMost5.length === 2);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template Length - The exact number of characters.\n * @param s - The string to check.\n * @param length - The exact number of characters.\n * @returns `true` if `s.length === length`, `false` otherwise. When `true`,\n * TypeScript narrows `s` to `FixedLengthString<Length> & S`.\n */\n export const isFixedLengthString = <\n S extends string,\n Length extends SupportedLength,\n >(\n s: S,\n length: Length,\n ): s is FixedLengthString<Length> & S => s.length === length;\n\n /**\n * Casts a string to `MinLengthString<MinLength>` after checking that it has\n * at least `minLength` characters.\n *\n * Use this instead of writing `s as unknown as MinLengthString<N>`: the\n * length is verified at runtime, and the original string type (e.g. literal\n * types) is preserved via intersection.\n *\n * @example\n *\n * ```ts\n * const password = Str.asMinLengthString('very-secret-password', 12);\n *\n * const nonEmpty: NonEmptyString = password; // OK (>= 1)\n *\n * assert.isTrue(nonEmpty.length >= 12);\n *\n * assert.throws(() => Str.asMinLengthString('short', 12)); // length 5 < 12\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @param s - The string to cast.\n * @param minLength - The minimum number of characters (inclusive).\n * @returns `s` typed as `MinLengthString<MinLength> & S`.\n * @throws {TypeError} If `s.length < minLength`.\n */\n export const asMinLengthString = <\n S extends string,\n MinLength extends SupportedLength,\n >(\n s: S,\n minLength: MinLength,\n ): MinLengthString<MinLength> & S => {\n if (!isMinLengthString(s, minLength)) {\n throw new TypeError(\n `Expected a string of length >= ${minLength}, got a string of length ${s.length}`,\n );\n }\n\n return s;\n };\n\n /**\n * Casts a string to `MaxLengthString<MaxLength>` after checking that it has\n * at most `maxLength` characters.\n *\n * Use this instead of writing `s as unknown as MaxLengthString<N>`: the\n * length is verified at runtime, and the original string type (e.g. literal\n * types) is preserved via intersection.\n *\n * @example\n *\n * ```ts\n * const userName = Str.asMaxLengthString('noshiro', 32);\n *\n * const relaxed: MaxLengthString<64> = userName; // OK (32 <= 64)\n *\n * assert.isTrue(relaxed.length <= 32);\n *\n * assert.throws(() => Str.asMaxLengthString('noshiro', 3)); // length 7 > 3\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to cast.\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `s` typed as `MaxLengthString<MaxLength> & S`.\n * @throws {TypeError} If `s.length > maxLength`.\n */\n export const asMaxLengthString = <\n S extends string,\n MaxLength extends SupportedLength,\n >(\n s: S,\n maxLength: MaxLength,\n ): MaxLengthString<MaxLength> & S => {\n if (!isMaxLengthString(s, maxLength)) {\n throw new TypeError(\n `Expected a string of length <= ${maxLength}, got a string of length ${s.length}`,\n );\n }\n\n return s;\n };\n\n /**\n * Casts a string to `BoundedLengthString<MinLength, MaxLength>` after\n * checking that its length is within the inclusive range\n * `[minLength, maxLength]`.\n *\n * Use this instead of writing `s as unknown as BoundedLengthString<M, N>`:\n * the length is verified at runtime, and the original string type (e.g.\n * literal types) is preserved via intersection.\n *\n * @example\n *\n * ```ts\n * const userId = Str.asBoundedLengthString('user-12345678', 8, 16);\n *\n * const relaxed: BoundedLengthString<1, 255> = userId; // OK ([8, 16] ⊆ [1, 255])\n *\n * assert.isTrue(relaxed.length >= 8 && relaxed.length <= 16);\n *\n * assert.throws(() => Str.asBoundedLengthString('user', 8, 16)); // length 4 < 8\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to cast.\n * @param minLength - The minimum number of characters (inclusive).\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `s` typed as `BoundedLengthString<MinLength, MaxLength> & S`.\n * @throws {TypeError} If `s.length < minLength` or `s.length > maxLength`.\n */\n export const asBoundedLengthString = <\n S extends string,\n MinLength extends SupportedLength,\n MaxLength extends SupportedLength,\n >(\n s: S,\n minLength: MinLength,\n maxLength: MaxLength,\n ): BoundedLengthString<MinLength, MaxLength> & S => {\n if (!isBoundedLengthString(s, minLength, maxLength)) {\n throw new TypeError(\n `Expected a string of length in [${minLength}, ${maxLength}], got a string of length ${s.length}`,\n );\n }\n\n return s;\n };\n\n /**\n * Casts a string to `FixedLengthString<Length>` after checking that it has\n * exactly `length` characters.\n *\n * Use this instead of writing `s as unknown as FixedLengthString<N>`: the\n * length is verified at runtime, and the original string type (e.g. literal\n * types) is preserved via intersection.\n *\n * @example\n *\n * ```ts\n * const countryCode = Str.asFixedLengthString('JP', 2);\n *\n * const atMost5: MaxLengthString<5> = countryCode; // OK (2 <= 5)\n *\n * assert.isTrue(atMost5.length === 2);\n *\n * assert.throws(() => Str.asFixedLengthString('JP', 3)); // length 2 !== 3\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template Length - The exact number of characters.\n * @param s - The string to cast.\n * @param length - The exact number of characters.\n * @returns `s` typed as `FixedLengthString<Length> & S`.\n * @throws {TypeError} If `s.length !== length`.\n */\n export const asFixedLengthString = <\n S extends string,\n Length extends SupportedLength,\n >(\n s: S,\n length: Length,\n ): FixedLengthString<Length> & S => {\n if (!isFixedLengthString(s, length)) {\n throw new TypeError(\n `Expected a string of length ${length}, got a string of length ${s.length}`,\n );\n }\n\n return s;\n };\n}\n"],"names":["Str"],"mappings":"AAiBO,IAAU;AAAA,CAAV,CAAUA,IAAAA,KAAV;AAmCE,EAAMA,KAAA,iBAAA,GAAoB,CAI/B,CAAA,EACA,SAAA,KACwC,EAAE,MAAA,IAAU,SAAA;AAoC/C,EAAMA,KAAA,iBAAA,GAAoB,CAI/B,CAAA,EACA,SAAA,KACwC,EAAE,MAAA,IAAU,SAAA;AAwC/C,EAAMA,IAAAA,CAAA,qBAAA,GAAwB,CAKnC,CAAA,EACA,SAAA,EACA,cAEA,CAAA,CAAE,MAAA,IAAU,SAAA,IAAa,CAAA,CAAE,MAAA,IAAU,SAAA;AAqChC,EAAMA,KAAA,mBAAA,GAAsB,CAIjC,CAAA,EACA,MAAA,KACuC,EAAE,MAAA,KAAW,MAAA;AA6B/C,EAAMA,IAAAA,CAAA,iBAAA,GAAoB,CAI/B,CAAA,EACA,SAAA,KACmC;AACnC,IAAA,IAAI,CAAA,IAACA,IAAAA,CAAA,iBAAA,EAAkB,CAAA,EAAG,SAAS,CAAA,EAAG;AACpC,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,CAAA,+BAAA,EAAkC,SAAS,CAAA,yBAAA,EAA4B,CAAA,CAAE,MAAM,CAAA;AAAA,OACjF;AAAA,IACF;AAEA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AA6BO,EAAMA,IAAAA,CAAA,iBAAA,GAAoB,CAI/B,CAAA,EACA,SAAA,KACmC;AACnC,IAAA,IAAI,CAAA,IAACA,IAAAA,CAAA,iBAAA,EAAkB,CAAA,EAAG,SAAS,CAAA,EAAG;AACpC,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,CAAA,+BAAA,EAAkC,SAAS,CAAA,yBAAA,EAA4B,CAAA,CAAE,MAAM,CAAA;AAAA,OACjF;AAAA,IACF;AAEA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAgCO,EAAMA,IAAAA,CAAA,qBAAA,GAAwB,CAKnC,CAAA,EACA,WACA,SAAA,KACkD;AAClD,IAAA,IAAI,KAACA,IAAAA,CAAA,qBAAA,EAAsB,CAAA,EAAG,SAAA,EAAW,SAAS,CAAA,EAAG;AACnD,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,mCAAmC,SAAS,CAAA,EAAA,EAAK,SAAS,CAAA,0BAAA,EAA6B,EAAE,MAAM,CAAA;AAAA,OACjG;AAAA,IACF;AAEA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AA6BO,EAAMA,IAAAA,CAAA,mBAAA,GAAsB,CAIjC,CAAA,EACA,MAAA,KACkC;AAClC,IAAA,IAAI,CAAA,IAACA,IAAAA,CAAA,mBAAA,EAAoB,CAAA,EAAG,MAAM,CAAA,EAAG;AACnC,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,CAAA,4BAAA,EAA+B,MAAM,CAAA,yBAAA,EAA4B,CAAA,CAAE,MAAM,CAAA;AAAA,OAC3E;AAAA,IACF;AAEA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAAA,CAAA,EAhWe,GAAA,KAAA,GAAA,GAAA,EAAA,CAAA,CAAA;;;;"}
|
|
1
|
+
{"version":3,"file":"str.mjs","sources":["../../src/string/str.mts"],"sourcesContent":["import {\n type BoundedLengthString,\n type FixedLengthString,\n type MaxLengthString,\n type MinLengthString,\n type NonEmptyString,\n type SupportedLength,\n} from 'ts-type-forge';\n\n/**\n * Namespace providing type guards and runtime-checked casts for the branded\n * length-constrained string types (`MinLengthString`, `MaxLengthString`,\n * `BoundedLengthString`, `FixedLengthString`).\n *\n * Lengths are measured in UTF-16 code units (the same unit as\n * `String.prototype.length`), so characters outside the Basic Multilingual\n * Plane (e.g. emoji) count as 2.\n */\nexport namespace Str {\n /**\n * Type guard that checks if a string has at least `minLength` characters.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `MinLengthString<MinLength>` while preserving the\n * original string type (e.g. literal types) via intersection.\n * - The result participates in the length-constraint subtyping relation: for\n * example, a value narrowed to `MinLengthString<3>` is assignable to\n * `MinLengthString<1>` (= `NonEmptyString`).\n *\n * @example\n *\n * ```ts\n * const input: string = 'very-secret-password';\n *\n * assert.isTrue(Str.isMinLengthString(input, 12));\n *\n * assert.isFalse(Str.isMinLengthString('short', 12));\n *\n * if (Str.isMinLengthString(input, 12)) {\n * const nonEmpty: NonEmptyString = input; // OK (12 >= 1)\n *\n * assert.isTrue(nonEmpty.length >= 12);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @param s - The string to check.\n * @param minLength - The minimum number of characters (inclusive).\n * @returns `true` if `s.length >= minLength`, `false` otherwise. When `true`,\n * TypeScript narrows `s` to `MinLengthString<MinLength> & S`.\n */\n export const isMinLengthString = <\n S extends string,\n MinLength extends SupportedLength,\n >(\n s: S,\n minLength: MinLength,\n ): s is MinLengthString<MinLength> & S => s.length >= minLength;\n\n /**\n * Type guard that checks if a string has at most `maxLength` characters.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `MaxLengthString<MaxLength>` while preserving the\n * original string type (e.g. literal types) via intersection.\n * - The result participates in the length-constraint subtyping relation: for\n * example, a value narrowed to `MaxLengthString<16>` is assignable to\n * `MaxLengthString<32>`.\n *\n * @example\n *\n * ```ts\n * const input: string = 'noshiro';\n *\n * assert.isTrue(Str.isMaxLengthString(input, 32));\n *\n * assert.isFalse(Str.isMaxLengthString(input, 3));\n *\n * if (Str.isMaxLengthString(input, 32)) {\n * const relaxed: MaxLengthString<64> = input; // OK (32 <= 64)\n *\n * assert.isTrue(relaxed.length <= 32);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to check.\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `true` if `s.length <= maxLength`, `false` otherwise. When `true`,\n * TypeScript narrows `s` to `MaxLengthString<MaxLength> & S`.\n */\n export const isMaxLengthString = <\n S extends string,\n MaxLength extends SupportedLength,\n >(\n s: S,\n maxLength: MaxLength,\n ): s is MaxLengthString<MaxLength> & S => s.length <= maxLength;\n\n /**\n * Type guard that checks if a string's length is within the inclusive range\n * `[minLength, maxLength]`.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `BoundedLengthString<MinLength, MaxLength>` while\n * preserving the original string type (e.g. literal types) via intersection.\n * - Since `BoundedLengthString` is the intersection of `MinLengthString` and\n * `MaxLengthString`, the narrowed value is assignable to both, and both\n * bounds can be weakened independently.\n *\n * @example\n *\n * ```ts\n * const input: string = 'user-12345678';\n *\n * assert.isTrue(Str.isBoundedLengthString(input, 8, 16));\n *\n * assert.isFalse(Str.isBoundedLengthString('user', 8, 16));\n *\n * if (Str.isBoundedLengthString(input, 8, 16)) {\n * const userId: BoundedLengthString<1, 255> = input; // OK ([8, 16] ⊆ [1, 255])\n *\n * assert.isTrue(userId.length >= 8 && userId.length <= 16);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to check.\n * @param minLength - The minimum number of characters (inclusive).\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `true` if `minLength <= s.length && s.length <= maxLength`, `false`\n * otherwise. When `true`, TypeScript narrows `s` to\n * `BoundedLengthString<MinLength, MaxLength> & S`.\n */\n export const isBoundedLengthString = <\n S extends string,\n MinLength extends SupportedLength,\n MaxLength extends SupportedLength,\n >(\n s: S,\n minLength: MinLength,\n maxLength: MaxLength,\n ): s is BoundedLengthString<MinLength, MaxLength> & S =>\n s.length >= minLength && s.length <= maxLength;\n\n /**\n * Type guard that checks if a string has exactly `length` characters.\n *\n * **Type Narrowing Behavior:**\n *\n * - Narrows the input to `FixedLengthString<Length>` while preserving the\n * original string type (e.g. literal types) via intersection.\n * - Since `FixedLengthString<Length>` is defined as\n * `BoundedLengthString<Length, Length>`, the narrowed value is assignable to\n * any `MinLengthString<N>` with `N <= Length` and any `MaxLengthString<N>`\n * with `N >= Length`.\n *\n * @example\n *\n * ```ts\n * const input: string = 'JP';\n *\n * assert.isTrue(Str.isFixedLengthString(input, 2));\n *\n * assert.isFalse(Str.isFixedLengthString(input, 3));\n *\n * if (Str.isFixedLengthString(input, 2)) {\n * const atMost5: MaxLengthString<5> = input; // OK (2 <= 5)\n *\n * assert.isTrue(atMost5.length === 2);\n * }\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template Length - The exact number of characters.\n * @param s - The string to check.\n * @param length - The exact number of characters.\n * @returns `true` if `s.length === length`, `false` otherwise. When `true`,\n * TypeScript narrows `s` to `FixedLengthString<Length> & S`.\n */\n export const isFixedLengthString = <\n S extends string,\n Length extends SupportedLength,\n >(\n s: S,\n length: Length,\n ): s is FixedLengthString<Length> & S => s.length === length;\n\n /**\n * Casts a string to `MinLengthString<MinLength>` after checking that it has\n * at least `minLength` characters.\n *\n * Use this instead of writing `s as unknown as MinLengthString<N>`: the\n * length is verified at runtime, and the original string type (e.g. literal\n * types) is preserved via intersection.\n *\n * Supports a curried overload: calling it with only `minLength` returns a\n * function that casts a string, which is convenient for `pipe`/`map`.\n *\n * @example\n *\n * ```ts\n * const password = Str.asMinLengthString('very-secret-password', 12);\n *\n * const nonEmpty: NonEmptyString = password; // OK (>= 1)\n *\n * assert.isTrue(nonEmpty.length >= 12);\n *\n * // curried version\n * const asPassword = Str.asMinLengthString(12);\n *\n * assert.strictEqual(asPassword('another-secret'), 'another-secret');\n *\n * assert.throws(() => Str.asMinLengthString('short', 12)); // length 5 < 12\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @param s - The string to cast.\n * @param minLength - The minimum number of characters (inclusive).\n * @returns `s` typed as `MinLengthString<MinLength> & S`.\n * @throws {TypeError} If `s.length < minLength`.\n */\n export function asMinLengthString<\n S extends string,\n MinLength extends SupportedLength,\n >(s: S, minLength: MinLength): MinLengthString<MinLength> & S;\n\n // Curried version\n export function asMinLengthString<MinLength extends SupportedLength>(\n minLength: MinLength,\n ): <S extends string>(s: S) => MinLengthString<MinLength> & S;\n\n export function asMinLengthString(\n ...args:\n | readonly [s: string, minLength: SupportedLength]\n | readonly [minLength: SupportedLength]\n ): string | ((s: string) => string) {\n switch (args.length) {\n case 2: {\n const [s, minLength] = args;\n\n if (!isMinLengthString(s, minLength)) {\n throw new TypeError(\n `Expected a string of length >= ${minLength}, got a string of length ${s.length}`,\n );\n }\n\n return s;\n }\n\n case 1: {\n const [minLength] = args;\n\n return (s) => asMinLengthString(s, minLength);\n }\n }\n }\n\n /**\n * Casts a string to `MaxLengthString<MaxLength>` after checking that it has\n * at most `maxLength` characters.\n *\n * Use this instead of writing `s as unknown as MaxLengthString<N>`: the\n * length is verified at runtime, and the original string type (e.g. literal\n * types) is preserved via intersection.\n *\n * Supports a curried overload: calling it with only `maxLength` returns a\n * function that casts a string, which is convenient for `pipe`/`map`.\n *\n * @example\n *\n * ```ts\n * const userName = Str.asMaxLengthString('noshiro', 32);\n *\n * const relaxed: MaxLengthString<64> = userName; // OK (32 <= 64)\n *\n * assert.isTrue(relaxed.length <= 32);\n *\n * // curried version\n * const asUserName = Str.asMaxLengthString(32);\n *\n * assert.strictEqual(asUserName('another-user'), 'another-user');\n *\n * assert.throws(() => Str.asMaxLengthString('noshiro', 3)); // length 7 > 3\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to cast.\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `s` typed as `MaxLengthString<MaxLength> & S`.\n * @throws {TypeError} If `s.length > maxLength`.\n */\n export function asMaxLengthString<\n S extends string,\n MaxLength extends SupportedLength,\n >(s: S, maxLength: MaxLength): MaxLengthString<MaxLength> & S;\n\n // Curried version\n export function asMaxLengthString<MaxLength extends SupportedLength>(\n maxLength: MaxLength,\n ): <S extends string>(s: S) => MaxLengthString<MaxLength> & S;\n\n export function asMaxLengthString(\n ...args:\n | readonly [s: string, maxLength: SupportedLength]\n | readonly [maxLength: SupportedLength]\n ): string | ((s: string) => string) {\n switch (args.length) {\n case 2: {\n const [s, maxLength] = args;\n\n if (!isMaxLengthString(s, maxLength)) {\n throw new TypeError(\n `Expected a string of length <= ${maxLength}, got a string of length ${s.length}`,\n );\n }\n\n return s;\n }\n\n case 1: {\n const [maxLength] = args;\n\n return (s) => asMaxLengthString(s, maxLength);\n }\n }\n }\n\n /**\n * Casts a string to `BoundedLengthString<MinLength, MaxLength>` after\n * checking that its length is within the inclusive range\n * `[minLength, maxLength]`.\n *\n * Use this instead of writing `s as unknown as BoundedLengthString<M, N>`:\n * the length is verified at runtime, and the original string type (e.g.\n * literal types) is preserved via intersection.\n *\n * Supports a curried overload: calling it with only `minLength` and\n * `maxLength` returns a function that casts a string, which is convenient for\n * `pipe`/`map`.\n *\n * @example\n *\n * ```ts\n * const userId = Str.asBoundedLengthString('user-12345678', 8, 16);\n *\n * const relaxed: BoundedLengthString<1, 255> = userId; // OK ([8, 16] ⊆ [1, 255])\n *\n * assert.isTrue(relaxed.length >= 8 && relaxed.length <= 16);\n *\n * // curried version\n * const asUserId = Str.asBoundedLengthString(8, 16);\n *\n * assert.strictEqual(asUserId('user-87654321'), 'user-87654321');\n *\n * assert.throws(() => Str.asBoundedLengthString('user', 8, 16)); // length 4 < 8\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template MinLength - The minimum number of characters (inclusive).\n * @template MaxLength - The maximum number of characters (inclusive).\n * @param s - The string to cast.\n * @param minLength - The minimum number of characters (inclusive).\n * @param maxLength - The maximum number of characters (inclusive).\n * @returns `s` typed as `BoundedLengthString<MinLength, MaxLength> & S`.\n * @throws {TypeError} If `s.length < minLength` or `s.length > maxLength`.\n */\n export function asBoundedLengthString<\n S extends string,\n MinLength extends SupportedLength,\n MaxLength extends SupportedLength,\n >(\n s: S,\n minLength: MinLength,\n maxLength: MaxLength,\n ): BoundedLengthString<MinLength, MaxLength> & S;\n\n // Curried version\n export function asBoundedLengthString<\n MinLength extends SupportedLength,\n MaxLength extends SupportedLength,\n >(\n minLength: MinLength,\n maxLength: MaxLength,\n ): <S extends string>(s: S) => BoundedLengthString<MinLength, MaxLength> & S;\n\n export function asBoundedLengthString(\n ...args:\n | readonly [\n s: string,\n minLength: SupportedLength,\n maxLength: SupportedLength,\n ]\n | readonly [minLength: SupportedLength, maxLength: SupportedLength]\n ): string | ((s: string) => string) {\n switch (args.length) {\n case 3: {\n const [s, minLength, maxLength] = args;\n\n if (!isBoundedLengthString(s, minLength, maxLength)) {\n throw new TypeError(\n `Expected a string of length in [${minLength}, ${maxLength}], got a string of length ${s.length}`,\n );\n }\n\n return s;\n }\n\n case 2: {\n const [minLength, maxLength] = args;\n\n return (s) => asBoundedLengthString(s, minLength, maxLength);\n }\n }\n }\n\n /**\n * Casts a string to `FixedLengthString<Length>` after checking that it has\n * exactly `length` characters.\n *\n * Use this instead of writing `s as unknown as FixedLengthString<N>`: the\n * length is verified at runtime, and the original string type (e.g. literal\n * types) is preserved via intersection.\n *\n * Supports a curried overload: calling it with only `length` returns a\n * function that casts a string, which is convenient for `pipe`/`map`.\n *\n * @example\n *\n * ```ts\n * const countryCode = Str.asFixedLengthString('JP', 2);\n *\n * const atMost5: MaxLengthString<5> = countryCode; // OK (2 <= 5)\n *\n * assert.isTrue(atMost5.length === 2);\n *\n * // curried version\n * const asCountryCode = Str.asFixedLengthString(2);\n *\n * assert.strictEqual(asCountryCode('US'), 'US');\n *\n * assert.throws(() => Str.asFixedLengthString('JP', 3)); // length 2 !== 3\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @template Length - The exact number of characters.\n * @param s - The string to cast.\n * @param length - The exact number of characters.\n * @returns `s` typed as `FixedLengthString<Length> & S`.\n * @throws {TypeError} If `s.length !== length`.\n */\n export function asFixedLengthString<\n S extends string,\n Length extends SupportedLength,\n >(s: S, length: Length): FixedLengthString<Length> & S;\n\n // Curried version\n export function asFixedLengthString<Length extends SupportedLength>(\n length: Length,\n ): <S extends string>(s: S) => FixedLengthString<Length> & S;\n\n export function asFixedLengthString(\n ...args:\n | readonly [s: string, length: SupportedLength]\n | readonly [length: SupportedLength]\n ): string | ((s: string) => string) {\n switch (args.length) {\n case 2: {\n const [s, length] = args;\n\n if (!isFixedLengthString(s, length)) {\n throw new TypeError(\n `Expected a string of length ${length}, got a string of length ${s.length}`,\n );\n }\n\n return s;\n }\n\n case 1: {\n const [length] = args;\n\n return (s) => asFixedLengthString(s, length);\n }\n }\n }\n\n /**\n * Casts a string to `NonEmptyString` after checking that it has at least one\n * character.\n *\n * `NonEmptyString` is an alias of `MinLengthString<1>`, so this is the\n * length-1 specialization of {@link asMinLengthString}. Use this instead of\n * writing `s as unknown as NonEmptyString`: the length is verified at\n * runtime, and the original string type (e.g. literal types) is preserved\n * via intersection.\n *\n * @example\n *\n * ```ts\n * const name = Str.asNonEmptyString('noshiro');\n *\n * const nonEmpty: NonEmptyString = name; // OK\n *\n * assert.isTrue(nonEmpty.length >= 1);\n *\n * assert.throws(() => Str.asNonEmptyString('')); // length 0 < 1\n * ```\n *\n * @template S - The input string type (literal types are preserved).\n * @param s - The string to cast.\n * @returns `s` typed as `NonEmptyString & S`.\n * @throws {TypeError} If `s` is empty.\n */\n export const asNonEmptyString = <S extends string>(\n s: S,\n ): NonEmptyString & S => {\n if (!isMinLengthString(s, 1)) {\n throw new TypeError('Expected a non-empty string, got an empty string');\n }\n\n return s;\n };\n}\n"],"names":["Str"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type FixedLengthArray,
|
|
4
4
|
type MaxLengthArray,
|
|
5
5
|
type MinLengthArray,
|
|
6
|
+
type NonEmptyArray,
|
|
6
7
|
type SupportedLength,
|
|
7
8
|
} from 'ts-type-forge';
|
|
8
9
|
import {
|
|
@@ -22,6 +23,9 @@ import {
|
|
|
22
23
|
* structural tuple prefix (in-range indexed access without `undefined` under
|
|
23
24
|
* `noUncheckedIndexedAccess`).
|
|
24
25
|
*
|
|
26
|
+
* Supports a curried overload: calling it with only `length` returns a
|
|
27
|
+
* function that casts an array, which is convenient for `pipe`/`map`.
|
|
28
|
+
*
|
|
25
29
|
* @example
|
|
26
30
|
*
|
|
27
31
|
* ```ts
|
|
@@ -30,6 +34,10 @@ import {
|
|
|
30
34
|
* const atMost5: MaxLengthArray<5, number> = rgb; // OK (3 <= 5)
|
|
31
35
|
* const red: number = rgb[0]; // OK — no `undefined`
|
|
32
36
|
*
|
|
37
|
+
* // curried version
|
|
38
|
+
* const asRgb = Arr.asFixedLengthArray(3);
|
|
39
|
+
* const green = asRgb([0, 255, 0]);
|
|
40
|
+
*
|
|
33
41
|
* // Arr.asFixedLengthArray([255, 128], 3); // throws TypeError
|
|
34
42
|
* ```
|
|
35
43
|
*
|
|
@@ -40,21 +48,43 @@ import {
|
|
|
40
48
|
* @returns `xs` typed as `FixedLengthArray<Length, Xs[number]> & Xs`.
|
|
41
49
|
* @throws {TypeError} If `xs.length !== length`.
|
|
42
50
|
*/
|
|
43
|
-
export
|
|
51
|
+
export function asFixedLengthArray<
|
|
44
52
|
Xs extends readonly unknown[],
|
|
45
53
|
Length extends SupportedLength,
|
|
46
|
-
>(
|
|
47
|
-
|
|
54
|
+
>(xs: Xs, length: Length): FixedLengthArray<Length, Xs[number]> & Xs;
|
|
55
|
+
|
|
56
|
+
// Curried version
|
|
57
|
+
export function asFixedLengthArray<Length extends SupportedLength>(
|
|
48
58
|
length: Length,
|
|
49
|
-
):
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`Expected an array of length ${length}, got an array of length ${xs.length}`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
59
|
+
): <Xs extends readonly unknown[]>(
|
|
60
|
+
xs: Xs,
|
|
61
|
+
) => FixedLengthArray<Length, Xs[number]> & Xs;
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
export function asFixedLengthArray<E>(
|
|
64
|
+
...args:
|
|
65
|
+
| readonly [xs: readonly E[], length: SupportedLength]
|
|
66
|
+
| readonly [length: SupportedLength]
|
|
67
|
+
): readonly E[] | ((xs: readonly E[]) => readonly E[]) {
|
|
68
|
+
switch (args.length) {
|
|
69
|
+
case 2: {
|
|
70
|
+
const [xs, length] = args;
|
|
71
|
+
|
|
72
|
+
if (!isFixedLengthArray(xs, length)) {
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
`Expected an array of length ${length}, got an array of length ${xs.length}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return xs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 1: {
|
|
82
|
+
const [length] = args;
|
|
83
|
+
|
|
84
|
+
return (xs) => asFixedLengthArray(xs, length);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
58
88
|
|
|
59
89
|
/**
|
|
60
90
|
* Casts an array to `MinLengthArray<MinLength, E>` after checking that it has
|
|
@@ -66,6 +96,9 @@ export const asFixedLengthArray = <
|
|
|
66
96
|
* structural tuple prefix (indexed access below `min(N, 10)` without
|
|
67
97
|
* `undefined` under `noUncheckedIndexedAccess`).
|
|
68
98
|
*
|
|
99
|
+
* Supports a curried overload: calling it with only `minLength` returns a
|
|
100
|
+
* function that casts an array, which is convenient for `pipe`/`map`.
|
|
101
|
+
*
|
|
69
102
|
* @example
|
|
70
103
|
*
|
|
71
104
|
* ```ts
|
|
@@ -74,6 +107,10 @@ export const asFixedLengthArray = <
|
|
|
74
107
|
* const nonEmpty: MinLengthArray<1, number> = history; // OK (3 >= 1)
|
|
75
108
|
* const first: number = history[0]; // OK — no `undefined`
|
|
76
109
|
*
|
|
110
|
+
* // curried version
|
|
111
|
+
* const asHistory = Arr.asMinLengthArray(3);
|
|
112
|
+
* const next = asHistory([4, 5, 6, 7]);
|
|
113
|
+
*
|
|
77
114
|
* // Arr.asMinLengthArray([0], 3); // throws TypeError
|
|
78
115
|
* ```
|
|
79
116
|
*
|
|
@@ -84,21 +121,43 @@ export const asFixedLengthArray = <
|
|
|
84
121
|
* @returns `xs` typed as `MinLengthArray<MinLength, Xs[number]> & Xs`.
|
|
85
122
|
* @throws {TypeError} If `xs.length < minLength`.
|
|
86
123
|
*/
|
|
87
|
-
export
|
|
124
|
+
export function asMinLengthArray<
|
|
88
125
|
Xs extends readonly unknown[],
|
|
89
126
|
MinLength extends SupportedLength,
|
|
90
|
-
>(
|
|
91
|
-
|
|
127
|
+
>(xs: Xs, minLength: MinLength): MinLengthArray<MinLength, Xs[number]> & Xs;
|
|
128
|
+
|
|
129
|
+
// Curried version
|
|
130
|
+
export function asMinLengthArray<MinLength extends SupportedLength>(
|
|
92
131
|
minLength: MinLength,
|
|
93
|
-
):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`Expected an array of length >= ${minLength}, got an array of length ${xs.length}`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
132
|
+
): <Xs extends readonly unknown[]>(
|
|
133
|
+
xs: Xs,
|
|
134
|
+
) => MinLengthArray<MinLength, Xs[number]> & Xs;
|
|
99
135
|
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
export function asMinLengthArray<E>(
|
|
137
|
+
...args:
|
|
138
|
+
| readonly [xs: readonly E[], minLength: SupportedLength]
|
|
139
|
+
| readonly [minLength: SupportedLength]
|
|
140
|
+
): readonly E[] | ((xs: readonly E[]) => readonly E[]) {
|
|
141
|
+
switch (args.length) {
|
|
142
|
+
case 2: {
|
|
143
|
+
const [xs, minLength] = args;
|
|
144
|
+
|
|
145
|
+
if (!isMinLengthArray(xs, minLength)) {
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
`Expected an array of length >= ${minLength}, got an array of length ${xs.length}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return xs;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 1: {
|
|
155
|
+
const [minLength] = args;
|
|
156
|
+
|
|
157
|
+
return (xs) => asMinLengthArray(xs, minLength);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
102
161
|
|
|
103
162
|
/**
|
|
104
163
|
* Casts an array to `MaxLengthArray<MaxLength, E>` after checking that it has
|
|
@@ -108,6 +167,9 @@ export const asMinLengthArray = <
|
|
|
108
167
|
* length is verified at runtime, and the original array type (e.g. tuple
|
|
109
168
|
* types) is preserved via intersection.
|
|
110
169
|
*
|
|
170
|
+
* Supports a curried overload: calling it with only `maxLength` returns a
|
|
171
|
+
* function that casts an array, which is convenient for `pipe`/`map`.
|
|
172
|
+
*
|
|
111
173
|
* @example
|
|
112
174
|
*
|
|
113
175
|
* ```ts
|
|
@@ -115,6 +177,10 @@ export const asMinLengthArray = <
|
|
|
115
177
|
*
|
|
116
178
|
* const relaxed: MaxLengthArray<16, string> = tags; // OK (8 <= 16)
|
|
117
179
|
*
|
|
180
|
+
* // curried version
|
|
181
|
+
* const asTags = Arr.asMaxLengthArray(8);
|
|
182
|
+
* const more = asTags(['d', 'e']);
|
|
183
|
+
*
|
|
118
184
|
* // Arr.asMaxLengthArray(['a', 'b', 'c'], 2); // throws TypeError
|
|
119
185
|
* ```
|
|
120
186
|
*
|
|
@@ -125,21 +191,43 @@ export const asMinLengthArray = <
|
|
|
125
191
|
* @returns `xs` typed as `MaxLengthArray<MaxLength, Xs[number]> & Xs`.
|
|
126
192
|
* @throws {TypeError} If `xs.length > maxLength`.
|
|
127
193
|
*/
|
|
128
|
-
export
|
|
194
|
+
export function asMaxLengthArray<
|
|
129
195
|
Xs extends readonly unknown[],
|
|
130
196
|
MaxLength extends SupportedLength,
|
|
131
|
-
>(
|
|
132
|
-
|
|
197
|
+
>(xs: Xs, maxLength: MaxLength): MaxLengthArray<MaxLength, Xs[number]> & Xs;
|
|
198
|
+
|
|
199
|
+
// Curried version
|
|
200
|
+
export function asMaxLengthArray<MaxLength extends SupportedLength>(
|
|
133
201
|
maxLength: MaxLength,
|
|
134
|
-
):
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`Expected an array of length <= ${maxLength}, got an array of length ${xs.length}`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
202
|
+
): <Xs extends readonly unknown[]>(
|
|
203
|
+
xs: Xs,
|
|
204
|
+
) => MaxLengthArray<MaxLength, Xs[number]> & Xs;
|
|
140
205
|
|
|
141
|
-
|
|
142
|
-
|
|
206
|
+
export function asMaxLengthArray<E>(
|
|
207
|
+
...args:
|
|
208
|
+
| readonly [xs: readonly E[], maxLength: SupportedLength]
|
|
209
|
+
| readonly [maxLength: SupportedLength]
|
|
210
|
+
): readonly E[] | ((xs: readonly E[]) => readonly E[]) {
|
|
211
|
+
switch (args.length) {
|
|
212
|
+
case 2: {
|
|
213
|
+
const [xs, maxLength] = args;
|
|
214
|
+
|
|
215
|
+
if (!isMaxLengthArray(xs, maxLength)) {
|
|
216
|
+
throw new TypeError(
|
|
217
|
+
`Expected an array of length <= ${maxLength}, got an array of length ${xs.length}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return xs;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case 1: {
|
|
225
|
+
const [maxLength] = args;
|
|
226
|
+
|
|
227
|
+
return (xs) => asMaxLengthArray(xs, maxLength);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
143
231
|
|
|
144
232
|
/**
|
|
145
233
|
* Casts an array to `BoundedLengthArray<MinLength, MaxLength, E>` after
|
|
@@ -151,6 +239,10 @@ export const asMaxLengthArray = <
|
|
|
151
239
|
* at runtime, and the original array type (e.g. tuple types) is preserved via
|
|
152
240
|
* intersection.
|
|
153
241
|
*
|
|
242
|
+
* Supports a curried overload: calling it with only `minLength` and
|
|
243
|
+
* `maxLength` returns a function that casts an array, which is convenient for
|
|
244
|
+
* `pipe`/`map`.
|
|
245
|
+
*
|
|
154
246
|
* @example
|
|
155
247
|
*
|
|
156
248
|
* ```ts
|
|
@@ -158,6 +250,10 @@ export const asMaxLengthArray = <
|
|
|
158
250
|
*
|
|
159
251
|
* const relaxed: BoundedLengthArray<0, 100, number> = selection; // OK
|
|
160
252
|
*
|
|
253
|
+
* // curried version
|
|
254
|
+
* const asSelection = Arr.asBoundedLengthArray(1, 5);
|
|
255
|
+
* const next = asSelection([4, 5]);
|
|
256
|
+
*
|
|
161
257
|
* // Arr.asBoundedLengthArray([], 1, 5); // throws TypeError
|
|
162
258
|
* ```
|
|
163
259
|
*
|
|
@@ -171,7 +267,7 @@ export const asMaxLengthArray = <
|
|
|
171
267
|
* `BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs`.
|
|
172
268
|
* @throws {TypeError} If `xs.length < minLength || xs.length > maxLength`.
|
|
173
269
|
*/
|
|
174
|
-
export
|
|
270
|
+
export function asBoundedLengthArray<
|
|
175
271
|
Xs extends readonly unknown[],
|
|
176
272
|
MinLength extends SupportedLength,
|
|
177
273
|
MaxLength extends SupportedLength,
|
|
@@ -179,11 +275,80 @@ export const asBoundedLengthArray = <
|
|
|
179
275
|
xs: Xs,
|
|
180
276
|
minLength: MinLength,
|
|
181
277
|
maxLength: MaxLength,
|
|
182
|
-
): BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
278
|
+
): BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs;
|
|
279
|
+
|
|
280
|
+
// Curried version
|
|
281
|
+
export function asBoundedLengthArray<
|
|
282
|
+
MinLength extends SupportedLength,
|
|
283
|
+
MaxLength extends SupportedLength,
|
|
284
|
+
>(
|
|
285
|
+
minLength: MinLength,
|
|
286
|
+
maxLength: MaxLength,
|
|
287
|
+
): <Xs extends readonly unknown[]>(
|
|
288
|
+
xs: Xs,
|
|
289
|
+
) => BoundedLengthArray<MinLength, MaxLength, Xs[number]> & Xs;
|
|
290
|
+
|
|
291
|
+
export function asBoundedLengthArray<E>(
|
|
292
|
+
...args:
|
|
293
|
+
| readonly [
|
|
294
|
+
xs: readonly E[],
|
|
295
|
+
minLength: SupportedLength,
|
|
296
|
+
maxLength: SupportedLength,
|
|
297
|
+
]
|
|
298
|
+
| readonly [minLength: SupportedLength, maxLength: SupportedLength]
|
|
299
|
+
): readonly E[] | ((xs: readonly E[]) => readonly E[]) {
|
|
300
|
+
switch (args.length) {
|
|
301
|
+
case 3: {
|
|
302
|
+
const [xs, minLength, maxLength] = args;
|
|
303
|
+
|
|
304
|
+
if (!isBoundedLengthArray(xs, minLength, maxLength)) {
|
|
305
|
+
throw new TypeError(
|
|
306
|
+
`Expected an array of length in [${minLength}, ${maxLength}], got an array of length ${xs.length}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return xs;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
case 2: {
|
|
314
|
+
const [minLength, maxLength] = args;
|
|
315
|
+
|
|
316
|
+
return (xs) => asBoundedLengthArray(xs, minLength, maxLength);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Casts an array to `NonEmptyArray<E>` after checking that it has at least one
|
|
323
|
+
* element.
|
|
324
|
+
*
|
|
325
|
+
* `NonEmptyArray<E>` is an alias of `MinLengthArray<1, E>`, so this is the
|
|
326
|
+
* length-1 specialization of {@link asMinLengthArray}. Use this instead of
|
|
327
|
+
* writing `xs as unknown as NonEmptyArray<E>`: the length is verified at
|
|
328
|
+
* runtime, and the original array type (e.g. tuple types) is preserved via
|
|
329
|
+
* intersection, so the result also benefits from the structural tuple prefix
|
|
330
|
+
* (`xs[0]` without `undefined` under `noUncheckedIndexedAccess`).
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
*
|
|
334
|
+
* ```ts
|
|
335
|
+
* const history = Arr.asNonEmptyArray([0, 1, 2, 3]);
|
|
336
|
+
*
|
|
337
|
+
* const first: number = history[0]; // OK — no `undefined`
|
|
338
|
+
*
|
|
339
|
+
* // Arr.asNonEmptyArray([]); // throws TypeError
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* @template Xs - The input array type (tuple types are preserved).
|
|
343
|
+
* @param xs - The array to cast.
|
|
344
|
+
* @returns `xs` typed as `NonEmptyArray<Xs[number]> & Xs`.
|
|
345
|
+
* @throws {TypeError} If `xs` is empty.
|
|
346
|
+
*/
|
|
347
|
+
export const asNonEmptyArray = <Xs extends readonly unknown[]>(
|
|
348
|
+
xs: Xs,
|
|
349
|
+
): NonEmptyArray<Xs[number]> & Xs => {
|
|
350
|
+
if (!isMinLengthArray(xs, 1)) {
|
|
351
|
+
throw new TypeError('Expected a non-empty array, got an empty array');
|
|
187
352
|
}
|
|
188
353
|
|
|
189
354
|
return xs;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type FixedLengthArray,
|
|
4
4
|
type MaxLengthArray,
|
|
5
5
|
type MinLengthArray,
|
|
6
|
+
type NonEmptyArray,
|
|
6
7
|
} from 'ts-type-forge';
|
|
7
8
|
import { expectType } from '../../expect-type.mjs';
|
|
8
9
|
import {
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
asFixedLengthArray,
|
|
11
12
|
asMaxLengthArray,
|
|
12
13
|
asMinLengthArray,
|
|
14
|
+
asNonEmptyArray,
|
|
13
15
|
} from './array-utils-length-bounded-array-cast.mjs';
|
|
14
16
|
|
|
15
17
|
describe(asFixedLengthArray, () => {
|
|
@@ -47,6 +49,29 @@ describe(asFixedLengthArray, () => {
|
|
|
47
49
|
|
|
48
50
|
expectType<typeof casted, FixedLengthArray<3, 1 | 2 | 3>>('<=');
|
|
49
51
|
});
|
|
52
|
+
|
|
53
|
+
test('curried version casts an array', () => {
|
|
54
|
+
const xs: readonly number[] = [0, 1, 2, 3] as const;
|
|
55
|
+
|
|
56
|
+
const casted = asFixedLengthArray(4)(xs);
|
|
57
|
+
|
|
58
|
+
expect(casted).toBe(xs);
|
|
59
|
+
|
|
60
|
+
expectType<typeof casted, FixedLengthArray<4, number>>('<=');
|
|
61
|
+
|
|
62
|
+
// in-range indexed access does not include `undefined`
|
|
63
|
+
const _first = casted[0];
|
|
64
|
+
|
|
65
|
+
expectType<typeof _first, number>('=');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('curried version throws when the length does not match', () => {
|
|
69
|
+
expect(() => asFixedLengthArray(4)([0, 1, 2])).toThrow(TypeError);
|
|
70
|
+
|
|
71
|
+
expect(() => asFixedLengthArray(4)([0, 1, 2])).toThrow(
|
|
72
|
+
'Expected an array of length 4, got an array of length 3',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
50
75
|
});
|
|
51
76
|
|
|
52
77
|
describe(asMinLengthArray, () => {
|
|
@@ -69,6 +94,26 @@ describe(asMinLengthArray, () => {
|
|
|
69
94
|
'Expected an array of length >= 3, got an array of length 1',
|
|
70
95
|
);
|
|
71
96
|
});
|
|
97
|
+
|
|
98
|
+
test('curried version casts an array', () => {
|
|
99
|
+
const xs: readonly number[] = [0, 1, 2, 3] as const;
|
|
100
|
+
|
|
101
|
+
const casted = asMinLengthArray(3)(xs);
|
|
102
|
+
|
|
103
|
+
expect(casted).toBe(xs);
|
|
104
|
+
|
|
105
|
+
expectType<typeof casted, MinLengthArray<3, number>>('<=');
|
|
106
|
+
|
|
107
|
+
expectType<typeof casted, MinLengthArray<1, number>>('<='); // 3 >= 1
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('curried version throws when the array is too short', () => {
|
|
111
|
+
expect(() => asMinLengthArray(3)([0])).toThrow(TypeError);
|
|
112
|
+
|
|
113
|
+
expect(() => asMinLengthArray(3)([0])).toThrow(
|
|
114
|
+
'Expected an array of length >= 3, got an array of length 1',
|
|
115
|
+
);
|
|
116
|
+
});
|
|
72
117
|
});
|
|
73
118
|
|
|
74
119
|
describe(asMaxLengthArray, () => {
|
|
@@ -91,6 +136,26 @@ describe(asMaxLengthArray, () => {
|
|
|
91
136
|
'Expected an array of length <= 2, got an array of length 3',
|
|
92
137
|
);
|
|
93
138
|
});
|
|
139
|
+
|
|
140
|
+
test('curried version casts an array', () => {
|
|
141
|
+
const xs: readonly number[] = [0, 1, 2] as const;
|
|
142
|
+
|
|
143
|
+
const casted = asMaxLengthArray(8)(xs);
|
|
144
|
+
|
|
145
|
+
expect(casted).toBe(xs);
|
|
146
|
+
|
|
147
|
+
expectType<typeof casted, MaxLengthArray<8, number>>('<=');
|
|
148
|
+
|
|
149
|
+
expectType<typeof casted, MaxLengthArray<16, number>>('<='); // 8 <= 16
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('curried version throws when the array is too long', () => {
|
|
153
|
+
expect(() => asMaxLengthArray(2)([0, 1, 2])).toThrow(TypeError);
|
|
154
|
+
|
|
155
|
+
expect(() => asMaxLengthArray(2)([0, 1, 2])).toThrow(
|
|
156
|
+
'Expected an array of length <= 2, got an array of length 3',
|
|
157
|
+
);
|
|
158
|
+
});
|
|
94
159
|
});
|
|
95
160
|
|
|
96
161
|
describe(asBoundedLengthArray, () => {
|
|
@@ -115,4 +180,65 @@ describe(asBoundedLengthArray, () => {
|
|
|
115
180
|
'Expected an array of length in [1, 5], got an array of length 6',
|
|
116
181
|
);
|
|
117
182
|
});
|
|
183
|
+
|
|
184
|
+
test('curried version casts an array', () => {
|
|
185
|
+
const xs: readonly number[] = [1, 2, 3] as const;
|
|
186
|
+
|
|
187
|
+
const casted = asBoundedLengthArray(1, 5)(xs);
|
|
188
|
+
|
|
189
|
+
expect(casted).toBe(xs);
|
|
190
|
+
|
|
191
|
+
expectType<typeof casted, BoundedLengthArray<1, 5, number>>('<=');
|
|
192
|
+
|
|
193
|
+
expectType<typeof casted, MinLengthArray<1, number>>('<=');
|
|
194
|
+
|
|
195
|
+
expectType<typeof casted, MaxLengthArray<5, number>>('<=');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('curried version throws when the length is out of the range', () => {
|
|
199
|
+
expect(() => asBoundedLengthArray(1, 5)([])).toThrow(TypeError);
|
|
200
|
+
|
|
201
|
+
expect(() => asBoundedLengthArray(1, 5)([1, 2, 3, 4, 5, 6])).toThrow(
|
|
202
|
+
'Expected an array of length in [1, 5], got an array of length 6',
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe(asNonEmptyArray, () => {
|
|
208
|
+
test('should return the input when the array is non-empty', () => {
|
|
209
|
+
const xs: readonly number[] = [0, 1, 2, 3] as const;
|
|
210
|
+
|
|
211
|
+
const casted = asNonEmptyArray(xs);
|
|
212
|
+
|
|
213
|
+
expect(casted).toBe(xs);
|
|
214
|
+
|
|
215
|
+
expectType<typeof casted, NonEmptyArray<number>>('<=');
|
|
216
|
+
|
|
217
|
+
expectType<typeof casted, MinLengthArray<1, number>>('<=');
|
|
218
|
+
|
|
219
|
+
// in-range indexed access does not include `undefined`
|
|
220
|
+
const _first = casted[0];
|
|
221
|
+
|
|
222
|
+
expectType<typeof _first, number>('=');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('should throw when the array is empty', () => {
|
|
226
|
+
expect(() => asNonEmptyArray([])).toThrow(TypeError);
|
|
227
|
+
|
|
228
|
+
expect(() => asNonEmptyArray([])).toThrow(
|
|
229
|
+
'Expected a non-empty array, got an empty array',
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('should preserve the original array type', () => {
|
|
234
|
+
const tuple = [1, 2, 3] as const;
|
|
235
|
+
|
|
236
|
+
const casted = asNonEmptyArray(tuple);
|
|
237
|
+
|
|
238
|
+
expect(casted).toBe(tuple);
|
|
239
|
+
|
|
240
|
+
expectType<typeof casted, readonly [1, 2, 3]>('<=');
|
|
241
|
+
|
|
242
|
+
expectType<typeof casted, NonEmptyArray<1 | 2 | 3>>('<=');
|
|
243
|
+
});
|
|
118
244
|
});
|