TypeScript > 4.1 固定长度字符串文字类型

TypeScript > 4.1 Fixed Length String Literal Type

TypeScript 团队在最近的更新(4.1 和 4.2)中一直在字符串文字类型方面做了大量工作。我想知道是否有一种方法可以输入固定长度的字符串。

例如

type LambdaServicePrefix = 'my-application-service';
type LambdaFunctionIdentifier = 'dark-matter-upgrader';
type LambdaFunctionName = `${LambdaServicePrefix}-${LambdaFunctionIdentifier}`; // error: longer than 32 characters...

我想象它会怎样,Array<64, string>;。 TypeScript 具有 Tuple 类型,因此作为一个数组,我可以固定数组的长度。 [string, ... string * 62, string].

type FutureLambdaIdType = `${LambdaServicePrefix}-${string[32]}`;

无法使用 Typescript 表示固定长度的字符串。有一个very upvoted proposal here,但是这个功能还没有发布。

如果长度很小,有一些解决方法,例如:

type Char = 'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z'
type String3 = `${Char}${Char}${Char}`
const a: String3 = 'aa'    // error
const b: String3 = 'bbbbb' // error
const c: String3 = 'ccc'   // OK
const d: String3 = 'abc'   // OK

但是您无法处理大长度,因为您会 运行 出现“表达式产生的联合类型太复杂而无法表示”错误。

无法通过键入或 typescript utils 来限制字符串的长度。

但是,您可以使用正则表达式来验证字符串(包括长度):

/^([a-zA-Z0-9_-]){1,64}$/

截至 TS 4.1,TypeScript 中仍然没有正则表达式验证的字符串类型。 Template literal types handle some, but not all, of the use cases for such regex types. If you have a situation like this where template literal types are insufficient, you might want to go to microsoft/TypeScript#41160 并描述您的用例。对于某些 N extends number,“最大长度为 N 个字符的字符串”的想法很容易用正则表达式类型表达,但用模板文字不容易实现。

不过,让我们看看我们能走多远。


一些主要障碍挡在路上。首先是 TypeScript 无法轻易地将所有小于 N 个字符的字符串集合表示为特定类型 StringsOfLengthUpTo<N>。从概念上讲,任何给定的 StringsOfLengthUpTo<N> 都是一个大联合,但由于编译器对超过 ~10,000 个成员的联合犹豫不决,因此您只能用这种方式描述最多几个字符的字符串。假设你想支持 7 位可打印 ASCII 的 95 个字符,你将能够表示 StringsOfLengthUpTo<0>StringsOfLengthUpTo<1>,甚至 StringsOfLengthUpTo<2>。但是 StringsOfLengthUpTo<3> 会超出编译器的能力,因为它将是一个拥有超过 800,000 名成员的联盟。所以我们不得不放弃特定类型。


相反,我们可以将我们的类型视为与 generics 一起使用的 约束 。想象一下,我们有一个像 TruncateTo<T, N> 这样的类型,它接受一个类型 T extends string 和一个 N extends number 和 returns T 截断为 N 个字符。那么也许我们可以以某种方式限制 T extends TruncateTo<T, N> 并且编译器会自动警告太长的字符串。 las,T extends TruncateTo<T, N> 将是一个循环约束,所以我们不能直接写它。但我们至少可以写一个像这样的辅助函数:

const atMostN = <N extends Number, T extends string>(len: N, str: TruncateTo<T, N>) => str;

然后您可以调用 atMostN(32, "someStringLiteral"),它会根据字符串文字参数的长度成功或发出警告。

如果不是第二个障碍,这确实是我要走的路:TypeScript 支持 recursive conditional types,但它有 浅递归限制 . TruncateTo<T, N> 的简单实现是递归的,将 T 分解为第一个字符 F 和字符串的其余部分 T2,然后计算 TrancateTo<T2, N2>其中 N2N 减一。我什至可以这样写(关于 TypeScript 如何不能轻易地对数字进行数学运算但它可以 add/remove 来自元组的元素):

type _L<N extends number, L extends any[] = []> =
    L['length'] extends N ? L : _L<N, [0, ...L]>;
type _T<T extends string, L extends any[]> =
    L extends [any, ...infer L2] ? T extends `${infer F}${infer T2}` ?
    `${F}${_T<T2, L2>}` : T : "";
type TruncateTo<T extends string, N extends number> = _T<T, _L<N>>  

这确实有效 N 至少比 3 长:

type Fifteen = TruncateTo<"12345678901234567890", 15>;
// type Fifteen = "123456789012345"

但是在你到达32之前你会遇到问题:

type TwentyFive = TruncateTo<"123456789012345678901234567", 25>; // error!
// -------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type instantiation is excessively deep and possibly infinite.
// type TwentyFive = `1234567890123456${any}` 

很有可能通过一些令人讨厌的争论,可以制作一个 TruncateTo 的版本,它可以避免太大的联合和太深的递归。在我短暂的尝试中,我失败了。


相反,我决定放弃采用任何 Ngeneral 解决方案,而只处理 N 的特定值:32 . 可以这样写反自然罪:

// document.write(Array.from({length: 32},(_, i) => 
//  "${infer "+String.fromCharCode(65+Math.floor(i/26))+String.fromCharCode(65+i%26)+"}").join(""))
type TruncateTo32<T extends string> = T extends
    `${infer AA}${infer AB}${infer AC}${infer AD}${infer AE}${infer AF}${infer AG}${infer AH}${infer AI}${infer AJ}${infer AK}${infer AL}${infer AM}${infer AN}${infer AO}${infer AP}${infer AQ}${infer AR}${infer AS}${infer AT}${infer AU}${infer AV}${infer AW}${infer AX}${infer AY}${infer AZ}${infer BA}${infer BB}${infer BC}${infer BD}${infer BE}${infer BF}${infer R}` ?
    T extends `${infer F}${R}` ? F : never : T
const atMost32 = <T extends string>(str: TruncateTo32<T>) => str;

当您必须编写代码来编写代码时,您就知道事情很糟糕。在这里,我让编译器写出一个大模板文字,分别推断前 32 个字符中的每一个,然后将之后的所有内容放入 R 中。如果失败,则字符串很好。否则,我将所有这 32 个字符重新打包为 F 和 return that.

现在可以测试了:

const okay = atMost32("ThisStringIs28CharactersLong");
type Okay = typeof okay; // "ThisStringIs28CharactersLong"

const bad = atMost32("ThisStringHasALengthOf34Characters"); // error!
// ----------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// '"ThisStringHasALengthOf34Characters"' is not assignable to parameter of type 
// '"ThisStringHasALengthOf34Characte"'.
type Bad = typeof bad; // "ThisStringHasALengthOf34Characte"

而且有效!


但代价是什么?您是否希望其他任何人都必须使用屏幕末尾的一行模板文字推理来维护代码?目前,这感觉像是处于或非常接近 TypeScript 可能的边缘。实际上,我可能会改用某种运行时检查。也许去 microsoft/TypeScript#41160 并说明为什么编译时字符串长度验证很重要。

Playground link to code