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

13

近期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]}`;

2
哎呀,我猜如果TS没有这么浅的递归限制,这应该很容易。我尝试的所有方法都在长度为64的字符串之前就失败了。也许我可以采用暴力方法,但那将会非常丑陋。 - jcalz
1
现在我能提供的最接近的是这个。你有兴趣让我写出来吗?还是你还抱着希望想要其他的东西? - jcalz
1
此外,您可能希望前往microsoft/TypeScript#41160描述您的用例,因为他们正在努力弄清楚人们实际上需要多少正则表达式验证的字符串类型。 - jcalz
是的,我的错,我指的是像TypeScript中的任何其他情况一样的TypeScript编译器错误。如果您编写了一个导致编译器错误的解决方案,我将把它标记为已接受的答案。 - hanneswidrig
看起来你的例子实际上也在做我想做的事情。 - hanneswidrig
显示剩余2条评论
4个回答

15

已更新以反映更好的递归条件类型支持

截至TS 4.7,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 number并返回截断为N个字符的T。然后我们可以约束T extends TruncateTo<T,N>,编译器会自动警告过长的字符串。

以前,浅层递归限制会阻止我们编写TruncateTo<T, N>,其中N大约大于20左右,但是TypeScript 4.5引入了对条件类型尾递归消除的支持。这意味着我们可以通过添加一些额外的累加器参数来编写TruncateTo<T, N>,如下所示:

type TruncateTo<T extends string, N extends number,
    L extends any[] = [], A extends string = ""> =
    N extends L['length'] ? A :
    T extends `${infer F}${infer R}` ? (
        TruncateTo<R, N, [0, ...L], `${A}${F}`>
    ) :
    A

这个方法通过使用一个A累加器来存储我们正在构建的字符串,并且使用一个类似数组的累加器L来跟踪A字符串的长度(字符串文字类型没有强类型的length属性,请参见相关请求ms/TS#34692)。我们逐个字符地构建A,直到我们要么用完原始字符串,要么达到了长度N。让我们看看它的实际应用:
type Fifteen = TruncateTo<"12345678901234567890", 15>;
// type Fifteen = "123456789012345"

type TwentyFive = TruncateTo<"123456789012345678901234567", 25>;
// type TwentyFive = "1234567890123456789012345"

我们无法直接编写 T extends TruncateTo<T, N>,因为TypeScript会抱怨这是一个循环约束。但是我们至少可以编写一个辅助函数,如下所示:
const atMostN = <T extends string, N extends number>(
    num: N, str: T extends TruncateTo<T, N> ? T : TruncateTo<T, N>
) => str;

然后你可以调用atMostN(32, "someStringLiteral"),它会根据字符串字面量参数的长度成功或警告。请注意,str输入是一种奇怪的条件类型,其唯一目的是避免循环约束。 Tstr推断出来,然后与TruncateTo<T,N>进行检查。如果成功,那太棒了。否则,我们将给str赋予TruncateTo<T,N>类型,并看到错误消息。 它的工作原理如下:

const okay = atMostN(32, "ThisStringIs28CharactersLong"); // okay
type Okay = typeof okay; // "ThisStringIs28CharactersLong"

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

这值得吗?或许吧。原始答案需要做一些不太好的事情,才能得到一个固定长度的检查。现在的答案还不错,但仍然需要大量努力才能获得编译时检查。所以,您可能仍然需要使用正则表达式验证的字符串类型。

代码的Playground链接


感谢您的出色工作,我已经在那个问题上发表了评论。在我的脑海中,定义字符串的特定长度似乎并不难,因为它只是一系列字符,而我们已经有了固定长度数组的元组类型。 - hanneswidrig
当然可以,但是对于字符串字面量和元组类型,没有内置的“split”或“join”函数,如果你自己构建它们,很快就会遇到递归和/或联合限制。字符串字面量也没有强类型长度属性,因此在这里没有简单的处理方法。 - jcalz

8

在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

但是由于会遇到“表达式产生的联合类型太复杂而无法表示”错误,因此您无法处理较大的长度。


1
type IsThirteen<T extends number> = 13 extends T ? true : never
type IsFifteen<T extends number> = 15 extends T ? true : never

type LengthOfString<S extends string, T extends string[] = []> = S extends `${string}${infer R}`
  ? LengthOfString<R, [...T, string]>
  : T['length'];

type IsLengthThirteenOrFifteen<T extends string> = true extends IsThirteen<LengthOfString<T>>
    ? T
    : true extends IsFifteen<LengthOfString<T>>
        ? T
        : never

function IsLengthThirteenOrFifteenGuard <T extends string>(a: IsLengthThirteenOrFifteen<T>) {
  return a;
}

const b = IsLengthThirteenOrFifteenGuard('1131111111111')

参考资料:


-2
无法通过输入或typescript工具来限制字符串的长度。
但是,您可以使用正则表达式验证字符串(包括长度):
/^([a-zA-Z0-9_-]){1,64}$/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接