模板字面量类型深度解析
模板字面量类型让 TypeScript 能在类型层面拼接、匹配、解析字符串,是路由参数、国际化 key、CSS 属性值的精确约束利器。 核心是占位符的笛卡尔积展开,配合 Uppercase、Capitalize 等内置工具与 infer 做模式匹配,告别运行时正则。
一句话概括
模板字面量类型让 TypeScript 的类型系统能”算字符串”——${Prefix}${Name} 在类型层面拼接、匹配、解析,从此路由参数、国际化 key、CSS 属性值都能在编译期被精确约束,告别运行时正则 + 祈祷。
核心知识点
1. 基础:类型空间的模板字符串拼接
1
2
3
4
5
6
7
8
9
10
// 联合类型占位符 → 自动笛卡尔积展开
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// 多联合类型 → 所有组合全枚举
type Lang = "zh" | "en";
type Region = "CN" | "US";
type Locale = `${Lang}-${Region}`;
// "zh-CN" | "zh-US" | "en-CN" | "en-US"(2×2=4 种)
⚠️ 规模警告:5 个各含 20 个成员的联合组合 = 3,200,000 种排列,超出 TS 编译器限制(~100K),会报 union too complex。日常 3-4 个属性的组合完全没问题。
2. 四个内置字符串工具类型
1
2
3
4
5
6
7
type A = Uppercase<"hello">; // "HELLO"
type B = Lowercase<"Hello">; // "hello"
type C = Capitalize<"hello">; // "Hello"
type D = Uncapitalize<"Hello">; // "hello"
// 这些是 TS 编译器内置的字符串操作,不是运行时的 .toUpperCase()
// 编译后完全消失,零运行时开销
3. infer + 模板字面量 = 字符串模式匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 提取路由前缀后的路径
type RoutePath<T extends string> =
T extends `/api/${infer P}` ? P : never;
type P = RoutePath<"/api/users">; // "users"
// 提取域名
type Host<T extends string> =
T extends `https://${infer H}/${string}` ? H : never;
type H = Host<"https://api.github.com/v3/repos">; // "api.github.com"
// 提取文件扩展名
type Ext<T extends string> =
T extends `${string}.${infer E}` ? E : never;
type E = Ext<"index.ts">; // "ts"
4. 递归模板字面量:KebabCase / Split
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// HelloWorld → hello-world
type KebabCase<S extends string> =
S extends `${infer First}${infer Rest}`
? Rest extends Uncapitalize<Rest>
? `${Lowercase<First>}${KebabCase<Rest>}` // 小写字母,继续
: `${Lowercase<First>}-${KebabCase<Rest>}` // 大写字母,插入分隔符
: S;
type K = KebabCase<"HelloWorld">; // "hello-world"
// 按分隔符拆成联合类型
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? Head | Split<Tail, D>
: S;
type Parts = Split<"a,b,c", ",">; // "a" | "b" | "c"
递归限制:TS 类型递归深度约 50 层,处理短字符串(路由、变量名)无压力,处理 100 字符的 URL 可能超限。
5. 实战:路由参数类型安全提取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从路由模式字符串提取参数对象类型
type RouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & RouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = RouteParams<"/user/:userId/post/:postId">;
// { userId: string } & { postId: string }
// → 等价于 { userId: string; postId: string }
// 实战使用:
// useParams<"/user/:id">() 返回 { id: string }
// react-router v6 / vue-router 4 底层就用类似技术做类型推导
其实你每天都在用
- Redux action type 生成:
`${slice}/INCREMENT`— 模板字面量类型保证 action type 不会拼错 - CSS 单位约束:
type Size = \${number}px` | `${number}rem`— 输入“10px”没问题,输“10”` 报错 - 国际化 key 类型:
`${Section}.${Key}`— 防止写成"header.titel"(拼错)而不是"header.title" - 事件名约束:
on${EventName}— 确保onClick/onFocus没有不存在的事件名 - Tailwind 类名类型:
`bg-${Color}-${Shade}`— 限制类名组合只能是已定义的 class
常见误解(FAQ)
❌ 误区 1:「${string} 等于 string」
${string} 只能作为模板字面量类型中的”通配符”使用。/api/${string} 匹配 /api/users、/api/v2/list,但不匹配 /api 或 /user/api。它是一个嵌在上下文中的模式匹配符,不能独立表示”任意字符串”。
❌ 误区 2:「模板字面量里的联合类型组合无限安全」
笛卡尔积会爆炸。A | B | C(3个)× X | Y | Z(3个)× 1 | 2 | 3(3个)= 27 种没问题。但 5 个维度各 20 个值 = 3.2M 种 → TS 直接拒绝计算。遇到 union too complex 错误先检查模板字面量的联合规模。
❌ 误区 3:「递归模板字面量可以处理任意长度的字符串」
TS 类型递归 ~50 层限制,处理路由路径(通常 ≤ 10 段)完全够用,但不能拿来处理长 URL 或大段文本。递归模板字面量本质是”玩具”——在可控输入上很强大,在无界输入上碰到递归深度限制就露馅了。
❌ 误区 4:「模板字面量类型等于 JS 的模板字符串」
一个是类型空间(编译时),一个是值空间(运行时)。语法相同(反引号 + ${}),但类型空间的 Uppercase<"hello"> 编译后无任何代码残留,值空间的 `hello ${name}` 编译后是 JS 字符串拼接。不要混淆两者的使用场景和能力。
一句话总结
模板字面量类型让 TS 从”检查字符串”进化到”理解字符串”——从此路由解析、key 校验、单位约束不再靠运行时抛异常,而是写完代码就红线提醒。