如何在Typescript中定义与正则表达式匹配的可变长度字符串类型?

62lalag4  于 2022-12-14  发布在  TypeScript
关注(0)|答案(1)|浏览(242)

如何创建表示十六进制字符串的类型?

let str: ByteString = "f1afe3"; // valid
let str1: ByteString = "fa1"    // invalid, hex string length should be even
let str2: ByteString = "hello"  //invalid, only hex allow
let str3: ByteString = "ffeeaa3300"; // valid

我找到了herehere的例子,但是它们都只允许固定长度的字符串。可以把它们扩展到任意长度的字符串吗?

bvuwiixz

bvuwiixz1#

TypeScript中没有特定的类型是这样工作的,在microsoft/TypeScript#6579中有一个长期存在的特性请求,要求 * 正则表达式验证的字符串类型 *,在这里您大概可以编写如下内容

// INVALID TYPESCRIPT, DO NOT TRY THIS:
type ByteString = r/^(?:[0-9a-fA-F][0-9a-fA-F])*$/

它就可以工作了。不幸的是TypeScript中没有这样的类型。你不能通过类型系统中的正则表达式来完成这一点。
引入模板文本类型后,上述问题已解决,因为它们允许对字符串文本类型进行操作。我们可以尝试将模板文本类型用于您的用例,但有几个主要问题阻止了我们。
首先,可以为有效的十六进制数字生成联合类型:

type _HexDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' |
    'A' | 'B' | 'C' | 'D' | 'E' | 'F'
type HexDigit = _HexDigit | Lowercase<_HexDigit>;

然后为有效的数字 * 对 * 编写一个类型:

type Nybble = `${HexDigit}${HexDigit}`;
/* type Nybble = "00" | "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | 
  "0A" | "0B" | "0C" | "0D" | "0E" | "0F" | "0a" | "0b" | "0c" | "0d" | "0e" | "0f" | 
  "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" | 
  "1A" | "1B" | "1C" | "1D" | "1E" | "1F" | "1a" | "1b" | "1c" | "1d" | "1e" | "1f" | 
  "20" | "21" | "22" | "23" | "24" | "25" | "26" | "27" | "28" | "29" | 
  "2A" | "2B" | "2C" | "2D" | "2E" | "2F" | ... */

但是你会被困在更深的地方。语言不允许你写循环模板文字类型,所以你不能这样做:

type ByteStringX = "" | `${Nybble}${ByteStringX}`; // error!
//   ~~~~~~~~~~~ <--
// Type alias 'ByteStringX' circularly references itself.

您可以尝试展开该循环,但一旦尝试,编译器就会抱怨结果联合太大:

type ByteString0 = "" | Nybble | `${Nybble}${Nybble}`; // error! 
// ----------------------------> ~~~~~~~~~~~~~~~~~~~~
// Expression produces a union type that is too complex to represent.

TypeScript中的联合只能容纳大约100,000个成员......并且有超过230,000个有效的四位十六进制字符串。所以我们被卡住了。没有办法将ByteString写为特定的类型。
我们 * 能 * 做的是编写一个generic类型ByteString<T>,它 * 验证 * 候选字符串类型T,使得T extends ByteString<T>当且仅当T是有效的字节字符串类型。因此,它的行为就像一个约束而不是一个类型。为了使用它,我们需要一个helper函数来推断泛型类型参数T。也就是说,代替

type ByteString = ...
const x: ByteString = "..."

你会有

type ByteString<T extends string> = ...
function byteString<T extends string> = ...
const x = byteString("...");

以下是它的工作原理:

type ByteString<T extends string, A extends string = ""> =
    T extends "" ? A :
    T extends `${infer D0 extends HexDigit}${infer D1 extends HexDigit}${infer R}`
    ? ByteString<R, `${A}${D0}${D1}`> : `${A}${Nybble}`

const byteString = <T extends string>(
    str: T extends ByteString<T> ? T : ByteString<T>
) => str;

所以ByteString<T>是一个尾递归条件类型,如果T是空字符串,那么我们接受它;这是基本情况。否则,我们尝试将T解析为前两个十六进制数字D0D1,然后是字符串R的其余部分。如果解析成功,则通过计算ByteString<R>进行递归。如果解析失败,则返回一个“close”有效字符串,这样,失败将产生一个有帮助的错误消息。
让我们来测试一下:

let str = byteString("f1afe3"); // okay
let str1 = byteString("fa1"); // error!
// Argument of type '"fa1"' is not assignable to parameter of type 
// '"fa00" | "fa01" | "fa02" | "fa03" | "fa04" | "fa05" | "fa06" | ...
let str2 = byteString("hello"); // error!
// Argument of type '"hello"' is not assignable to parameter of type '"00" | "01" | 
// "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | ...
let str3 = byteString("ffeeaa3300"); // okay

str0str3被接受,而str1str2生成错误消息,说明输入不合适。"fa1"失败,并与fa${Nybble}进行比较,因为"1"不是有效的数字对。"hello"失败,并与${Nybble}进行比较。因为"he"不是有效的数字对。
所以这是我们能得到的最接近你想要的东西。如果它适合你的用例,那就太好了。
如果不是,那么microsoft/TypeScript#41160中的正则表达式验证字符串类型仍然存在一个未解决的问题。您可能希望转到该问题,给予它一个👍,并描述您的用例以及为什么递归模板文本约束不足以满足要求。
Playground代码链接

相关问题