Yuzhe's Blog

yuzhes

字符串转数字 String to Number

字符串转数字 String to Number

题目链接

题目

将字符串字面量类型转换为数字字面量类型,行为类似 Number.parseInt

type Result = ToNumber<'123'>  // 期望结果:123

解答

type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never

只需一行。TypeScript 4.8 引入了模板字面量类型中的 infer ... extends 约束推断,使得这个问题几乎变得平凡。

工作原理

S extends `${infer N extends number}` ? N : never

这告诉 TypeScript:“尝试将字符串 S 解析为一个包含 number 的模板。如果能解析,就将那个数字推断为 N。”

S = '123' 时,TypeScript 识别出 '123' 匹配模板 `${number}` 并推断 N = 123 作为数字字面量类型。

推导过程:

ToNumber<'123'>
→ '123' extends `${infer N extends number}` ? N : never
→ N = 123
→ 123 ✓

ToNumber<'0'>
→ N = 0 ✓

ToNumber<'abc'>
→ 'abc' 不匹配 `${number}`
→ never ✓

深入分析

4.8 之前的解法

在 TypeScript 4.8 之前,infer ... extends 不存在。经典做法是用元组计数:

type ToNumber<S extends string, A extends any[] = []> =
  `${A['length']}` extends S
    ? A['length']
    : ToNumber<S, [...A, 1]>

这个方案从 0 开始,通过不断增长元组 A 直到其长度(转为字符串后)匹配 S。能用但在 ~999 附近会触发递归限制。

旧方案工作过程:

ToNumber<'3'>

A = []        → '0' extends '3'?否
A = [1]       → '1' extends '3'?否
A = [1,1]     → '2' extends '3'?否
A = [1,1,1]   → '3' extends '3'?是 → A['length'] = 3 ✓

为什么 infer N extends number 能工作

TypeScript 4.8 新增了在条件类型中约束推断类型的能力。在模板字面量位置:

`${infer N extends number}`

TypeScript 的模板字面量推断引擎会尝试将字符串解析为数字。如果成功,N 会被收窄为具体的数字字面量。支持的格式:

无法解析的情况

ToNumber<''>        // never(空字符串)
ToNumber<'abc'>     // never(不是数字)
ToNumber<'12px'>    // never(不像 parseInt,不支持部分解析)
ToNumber<' 42'>     // never(前导空格)

注意与 JavaScript 的 parseInt 不同,类型层面的版本要求整个字符串都是有效数字——不支持部分解析。

也适用于其他原始类型

同样的模式适用于布尔值和大整数:

type ToBool<S extends string> =
  S extends `${infer B extends boolean}` ? B : never

type ToBI<S extends string> =
  S extends `${infer N extends bigint}` ? N : never

type T1 = ToBool<'true'>   // true
type T2 = ToBool<'false'>  // false
type T3 = ToBI<'42'>       // 42n

负数和边界情况

type T1 = ToNumber<'-1'>       // -1 ✓
type T2 = ToNumber<'0'>        // 0 ✓
type T3 = ToNumber<'1e5'>      // never(不支持科学计数法)
type T4 = ToNumber<'Infinity'> // never

核心要点