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

han*_*rig 2 string string-literals typescript

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

前任。

type LambdaServicePrefix = 'my-application-service';
type LambdaFunctionIdentifier = 'dark-matter-upgrader';
type LambdaFunctionName = `${LambdaServicePrefix}-${LambdaFunctionIdentifier}`; // error: longer than 32 characters...
Run Code Online (Sandbox Code Playgroud)

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

type FutureLambdaIdType = `${LambdaServicePrefix}-${string[32]}`;
Run Code Online (Sandbox Code Playgroud)

Loh*_*eek 9

无法使用 Typescript 来表示固定长度的字符串。这里有一个非常受欢迎的提案,但这个功能仍然没有发布。

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

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
Run Code Online (Sandbox Code Playgroud)

但是您无法处理大长度,因为您将遇到“表达式生成的联合类型过于复杂而无法表示”错误。


jca*_*alz 5

从 TS 4.1 开始,TypeScript 中仍然没有正则表达式验证的字符串类型。 模板文字类型处理此类正则表达式类型的一些(但不是全部)用例。如果您遇到模板文字类型不足的情况,您可能需要转到microsoft/TypeScript#41160并描述您的用例。N对于某些人N extends number来说,“最大长度为字符的字符串”的想法很容易用正则表达式类型表达,但用模板文字很难实现。

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


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


相反,我们可以将我们的类型视为与泛型一起使用的约束。想象一下,我们有一个像这样的类型,它接受一个类型和一个并返回截断为字符。那么也许我们可以以某种方式进行约束,编译器会自动警告过长的字符串。唉,这将是一个循环约束,所以我们不能直接写出来。但是我们至少可以写一个这样的辅助函数:TruncateTo<T, N>T extends stringN extends numberTNT extends TruncateTo<T, N>T extends TruncateTo<T, N>

const atMostN = <N extends Number, T extends string>(len: N, str: TruncateTo<T, N>) => str;
Run Code Online (Sandbox Code Playgroud)

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

这确实是我要走的路线,如果不是因为第二个障碍:TypeScript 支持递归条件类型,但它具有浅递归限制。的天真实现TruncateTo<T, N>将是递归的,在那里你打破T它的第一个字符F和字符串的其余部分T2,然后计算TrancateTo<T2, N2>whereN2N减一。我什至可以这样写(有一些关于 TypeScript 无法轻松对数字进行数学运算但它可以从元组中添加/删除元素的细节):

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>>  
Run Code Online (Sandbox Code Playgroud)

这确实N3至少:

type Fifteen = TruncateTo<"12345678901234567890", 15>;
// type Fifteen = "123456789012345"
Run Code Online (Sandbox Code Playgroud)

但在你开始之前,32你会看到问题:

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

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


相反,我决定放弃采用 any的一般解决方案N,只处理一个特定的值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;
Run Code Online (Sandbox Code Playgroud)

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

现在我们可以测试一下:

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"
Run Code Online (Sandbox Code Playgroud)

它有效!


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

Playground 链接到代码