文章

模板字面量类型

模板字面量类型

一句话概括

模板字面量类型 = 类型空间的字符串拼接——${Prefix}${Name} 能在类型层面拼字符串、做模式匹配、解析路由,TypeScript 4.1 后类型系统才有了”字符串运算”的能力。

核心知识点

1. 基础:类型空间的 ${}

1
2
3
4
5
6
7
8
9
type EventName = "click" | "focus" | "blur";
type HandlerName = `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. 四个内置字符串工具类型

1
2
3
4
5
6
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
// 提取特定前缀后的路径
type ExtractPath<T extends string> = T extends `/api/${infer P}` ? P : never;
type P = ExtractPath<"/api/users">; // "users"

// 提取域名:匹配 https:// 到第一个 / 之间的部分
type Host<T extends string> = T extends `https://${infer H}/${string}` ? H : never;
type H = Host<"https://api.github.com/v3">; // "api.github.com"

4. 递归实现字符串工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// KebabCase: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"

// Split:按分隔符拆成联合类型
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"

5. 实战:路由参数提取

1
2
3
4
5
6
7
8
9
10
// 从路由模式提取参数对象类型
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/:id/post/:postId">;
// { id: string } & { postId: string }

「其实你每天都在用」

  1. Redux action type 生成${FETCH}_${USER}_${REQUEST} 类型的笛卡尔积完美建模
  2. CSS 属性值约束type Size = \${number}px` | `${number}rem``
  3. 国际化 key 类型`${Section}.${Key}` 防止拼错翻译 key
  4. 事件名约束`on${EventName}` 确保不会写出 onHover 这种不存在的处理函数
  5. 路由路径参数解析:react-router / vue-router 的类型推导底层就用到了模板字面量类型

常见误解(FAQ)

❌ 误区 1:${string} 匹配所有字符串,所以跟 string 一样

${string} 可以出现在模板中作为”通配符”,但最终结果仍是模板字面量类型,不是 string。比如 /api/${string} 匹配 /api/users 但不匹配 /api/users/api

❌ 误区 2:模板字面量里的联合类型总是安全的

2-3 个小联合类型组合没问题。但 5 个各含 20 个成员的联合类型拼在一起 = 3,200,000 个成员,超出 TS 编译器的约 10 万成员限制,会报 union type too complex

❌ 误区 3:递归模板字面量随便写

递归深度有约 50 层限制。处理很长的字符串(如 100 个字符的 URL)时递归可能超过限制。日常场景(如路由解析)完全够用。

❌ 误区 4:模板字面量类型跟 JS 模板字符串是同一个东西

一个是类型空间,一个是值空间。写法一样(都是反引号 + ${}),但前者只存在于编译时,后者存在于运行时。类型空间的 Uppercase<"hello"> 不会转成 JS 代码。

一句话总结

模板字面量类型让 TypeScript 的类型系统具备了”字符串计算”能力——从此路由参数、国际化 key、CSS 单位都能在编译期被精确约束,而不再是靠运行时正则 + 祈祷。

本文由作者按照 CC BY 4.0 进行授权