如何创建仅用数字描述字符串的类型

Ara*_*rat 12 types typescript

如何创建一个类型来描述只能包含数字的字符串?

我可以写这样的东西,但是如何描述动态长度字符串的类型而不声明每个长度?

type StringDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type StringNumber = 
    | StringDigit 
    | `${StringDigit}${StringDigit}` 
    | `${StringDigit}${StringDigit}${StringDigit}`
    | `${StringDigit}${StringDigit}${StringDigit}${StringDigit}` 
    | `${StringDigit}${StringDigit}${StringDigit}${StringDigit}${StringDigit}`;
Run Code Online (Sandbox Code Playgroud)

递归类型不起作用:

type StringDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6';
type StringNumber = StringDigit | `${StringDigit}${StringNumber}` // Error: Type alias 'StringNumber' circularly references itself.
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 17

目前无法string在 TypeScript 中将“仅包含数字的 a”表示为特定类型。以下是一些差点错过的内容:


`${number}`

模板文字类型对其中带有“洞”的“模式”模板文字有一些支持,如microsoft/TypeScript#40598所实现的。像这样的类型`${number}`被解释为“任何string可以通过强制number”产生的类型。尽管它表示模板文字表达式的类型,但它生成的类型不是“文字”,而是一种表示大量可能值的宽类型,而无需显式表示每个值。这就像如何string表示近乎无限数量的可能字符串,而无需明确跟踪每个字符串。

这有一些奇怪的地方,例如这样的模式文字当前不能用作对象的键类型,如microsoft/TypeScript#42192中报告的那样。(从 TS4.4 开始,您将能够通过索引签名将它们用作密钥类型,如microsoft/TypeScript#44512中实现的那样)

但你可以通过以下方式接近你想要的类型`${number}`

type StringNumber = `${number}`;

const good: StringNumber[] = [
  "0", "10", "25", "8675309"
];

const bad: StringNumber[] = [
  "zero", "b4", "23skiddoo" // error!
  //~~~~  ~~~~  ~~~~~~~~~~~
  //none of these are assignable to `${number}`
];
Run Code Online (Sandbox Code Playgroud)

当然,numberTypeScript 中的实际 s 不一定仅限于仅由数字组成。有小数点、指数标记、符号标记、基数标记,甚至大于 9 的十六进制数字。如果您真的只想要数字,那么这`${number}`对您不起作用:

const ugly: StringNumber[] = [
  "-1.234e+99", // no error, but contains non-digits 
  "0b101", // ditto
  "0xabcdef", // ditto
];
Run Code Online (Sandbox Code Playgroud)

不存在可以表示仅由数字组成的字符串的“宽”类型,虽然number很接近,但它不是


字符串文字的大联合

这与您当前正在采用的方法相同:不要尝试使用隐式表示所有仅数字字符串的宽类型,而是创建一个显式列出所有可接受的字符串文字的大联合。例如:

type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
type MaybeDigit = Digit | '';
type StringNumber = `${Digit}${MaybeDigit}${MaybeDigit}${MaybeDigit}`;

const good: StringNumber[] = [
  "0", "10", "25", "8675"
];
const bad: StringNumber[] = [
  "zero", "b4", "23skiddoo", "-1.234e+99", "0b101", "0xabcdef" // error!
]
Run Code Online (Sandbox Code Playgroud)

但是,当然,正如您所注意到的,您仅限于某个最大长度的字符串:

const ugly: StringNumber[] = [
  "8675309" // error, but all digits!
]
Run Code Online (Sandbox Code Playgroud)

而且这个最大长度很小。正如microsoft/TypeScript#40336中提到的,拉取请求实现模板文字:

联盟类型限制为少于 100,000 个成员,以下情况将导致错误:

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`;  // Error
Run Code Online (Sandbox Code Playgroud)

如果您确实需要长字符串,那么这种方法将不适合您。


通用约束

好的,所以没有特定的类型可以工作。让我们创建一个通用约束并表示StringNumber<T extends string>它采用字符串文字类型T检查它。如果有效,则满足约束,否则失败:

type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
type StringNumber<T extends string, V = T> = T extends Digit ? V :
  T extends `${Digit}${infer R}` ? StringNumber<R, V> : "123456789";
Run Code Online (Sandbox Code Playgroud)

这是递归条件类型StringNumber<"12345">将计算为"12345",但StringNumber<"oops">将计算为"123456789"(一些随机可接受的字符串)。

我们可以使用身份辅助函数来代替类型注释。如果函数接受输入就很好,否则就会出错。由于它是一个恒等函数,因此值被保留:

const stringNumber = <T extends string>(n: StringNumber<T, T>) => n;
Run Code Online (Sandbox Code Playgroud)

让我们测试一下:

stringNumber("0"); // okay
stringNumber("10"); // okay
stringNumber("8675309"); // okay
stringNumber("12345678909876543210"); // okay
stringNumber("zero"); // error!
stringNumber("b4"); // error!
stringNumber("23skiddoo"); // error!
stringNumber("0xabcdef"); // error!
Run Code Online (Sandbox Code Playgroud)

看起来不错。这些错误有点奇怪,例如"zero" is not assignable to "123456789",但至少它是一个错误。

我也不确定你需要支持多长的号码;存在递归限制,因此如果您开始输入真正庞大的数字,您最终会遇到它们:

stringNumber("123456789098765432101234567890987654321012345678909876543210"); // error!
// Type instantiation is excessively deep and possibly infinite.
Run Code Online (Sandbox Code Playgroud)

有多种方法可以重写递归类型以减少递归次数,但希望您不需要这个。

这仍然是“有惊无险”,因为它迫使您在您可能希望仅使用简单类型注释的地方拖动泛型类型参数。但如果我真的需要接受这样的纯数字字符串,我会推荐这种方法。

一种可能的缓解措施是,如果您只需要进行开发人员输入验证,之后您可以假设该值已被检查。在这种情况下,您可以在面向开发人员的代码中强制执行此类验证的约束,然后扩展到string在私有库实现中使用,并假设您已经验证了它:

function openLock<T extends string>(combo: StringNumber<T>): boolean {
  return openLockInternalImpl(combo);
}

function openLockInternalImpl(combo: string) {
  return (combo === "8675309");
}
Run Code Online (Sandbox Code Playgroud)

Playground 代码链接