目前无法将“仅包含数字的string”表示为 TypeScript 中的特定类型。以下是一些几乎未命中的情况:
`${number}`
模板文字类型对带有“孔”的“模式”模板文字有一些支持,如microsoft/TypeScript#40598 所实现。像`${number}` 这样的类型被解释为“任何可以通过强制number 产生的string”。即使它表示模板文字表达式的类型,它产生的类型也不是“文字”,而是表示大量可能值的宽类型,而没有明确表示每个值。这就像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}`
];
当然,TypeScript 中的实际numbers 不一定限于仅由数字组成。有小数点、指数标记、符号标记、基数标记,甚至大于 9 的十六进制数字。如果你真的只想要数字,那么`${number}` 不适合你:
const ugly: StringNumber[] = [
"-1.234e+99", // no error, but contains non-digits
"0b101", // ditto
"0xabcdef", // ditto
];
没有“宽”类型可以表示仅由数字组成的字符串,虽然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!
]
但是,当然,正如您所注意到的,您只能使用某个最大长度的字符串:
const ugly: StringNumber[] = [
"8675309" // error, but all digits!
]
而这个最大长度是small。正如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
如果你真的需要长字符串,这种方法不适合你。
通用约束
好的,所以没有特定的类型有效。让我们改为创建一个generic constraint 并表示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";
这是recursive conditional type。 StringNumber<"12345"> 将评估为"12345",但StringNumber<"oops"> 将评估为"123456789"(一些随机可接受的字符串)。
而且我们可以使用标识辅助函数来代替类型注释。如果函数接受输入就很好,否则会出错。因为它是一个恒等函数,所以值被保留:
const stringNumber = <T extends string>(n: StringNumber<T, T>) => n;
让我们测试一下:
stringNumber("0"); // okay
stringNumber("10"); // okay
stringNumber("8675309"); // okay
stringNumber("12345678909876543210"); // okay
stringNumber("zero"); // error!
stringNumber("b4"); // error!
stringNumber("23skiddoo"); // error!
stringNumber("0xabcdef"); // error!
看起来不错。错误有点奇怪,比如"zero" is not assignable to "123456789",但至少是个错误。
我也不确定您需要支持多长时间;有递归限制,所以如果你开始输入真正庞大的数字,你最终会遇到它们:
stringNumber("123456789098765432101234567890987654321012345678909876543210"); // error!
// Type instantiation is excessively deep and possibly infinite.
有一些方法可以重写递归类型以减少递归,但希望你不需要这个。
这仍然是一个“未遂事件”,因为它迫使您在您可能希望只使用简单类型注释的地方拖动泛型类型参数。但如果我真的需要接受这样的纯数字字符串,我会推荐这种方法。
一种可能的缓解方法是,如果您只需要它用于开发人员输入验证,之后您可以假设该值已被检查。在这种情况下,您可以在面向开发人员的代码中强制执行此类验证的约束,然后将其扩展到 string 以在您的私有库实现中使用,并假设您已经对其进行了验证:
function openLock<T extends string>(combo: StringNumber<T>): boolean {
return openLockInternalImpl(combo);
}
function openLockInternalImpl(combo: string) {
return (combo === "8675309");
}
Playground link to code