Yuzhe's Blog

yuzhes

字符串的长度

字符串的长度

挑战链接

题目

计算字符串字面量的长度,行为类似 String#length

type T0 = LengthOfString<"hello">      // 5
type T1 = LengthOfString<"">           // 0
type T2 = LengthOfString<"typescript"> // 10

解题

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

核心思路:TypeScript 类型系统无法直接读取字符串字面量的 .length 数值——但元组.length 是精确的数字字面量类型。所以我们把字符串逐字符转成元组,再返回 T['length']

逐步拆解

第一步 — 模板字面量 infer 分割

S extends `${infer F}${infer R}`

S = "hello" 时,TypeScript 推断 F = "h"R = "ello"。每次调用剥掉第一个字符,剩余部分继续递归。当 S = "" 时,模式匹配失败,走到 base case。

第二步 — 累积到元组

LengthOfString<R, [...T, F]>

每个字符都追加到累积器 T 中。处理 "hello" 的过程:

第三步 — 读取长度

S = "" 时,条件分支走 base case,返回 T['length']。元组长度是数字字面量类型,所以 ["h","e","l","l","o"]['length'] 求值为 5

深度解析

为什么不能直接用 S['length']

JavaScript 中 "hello".length === 5,但在 TypeScript 类型系统里:

type L = "hello"['length'] // string(不是 5!)

TypeScript 知道字符串有 .length 属性,但类型是 number,而不是具体的数字字面量。类型系统本身不追踪字符串的字符数量。

元组则不同,它的长度是精确的数字字面量:

type L = ["h","e","l","l","o"]['length'] // 5 ✅

因为元组类型在结构上是精确的——TypeScript 知道 ["h","e","l","l","o"] 恰好有 5 个元素。

累积器模式

T extends string[] = [] 是递归类型工具中经典的累积器(accumulator)模式。不是在递归回溯时构建结果,而是把状态向前传递:

// ❌ 无累积器 — 无法在类型层面做算术加法
type Bad<S extends string> = S extends `${infer F}${infer R}`
  ? 1 + Bad<R>  // TypeScript 不支持这样的类型算术
  : 0

// ✅ 有累积器 — 长度自然从元组中读取
type Good<S extends string, T extends string[] = []> = 
  S extends `${infer F}${infer R}`
    ? Good<R, [...T, F]>
    : T['length']

这个模式避免了对类型做算术,让元组本身充当计数器。

递归深度限制

TypeScript 的递归深度上限约为 ~1000 层。由于每个字符消耗一层递归,这个方案对长度 ≤ ~1000 的字符串字面量完全够用,实际场景几乎不会遇到限制。

infer F 为什么只匹配一个字符?

`${infer F}${infer R}` 中,F 为什么是单个字符而不是多个?

TypeScript 对模板字面量中的 infer 采用贪心+回溯策略:前面的 infer 尽量少匹配,后面的 infer 接收剩余部分。对于无分隔符的两段 infer,结果固定为 F = 第一个字符R = 剩余字符串,这是有保证的行为。

关键收获