模板字面量类型是 TypeScript 4.1 引入的一个能力。它的语法和 JavaScript 模板字符串一样——用反引号和 ${} 插值——但运行在类型空间而不是值空间。它不是运行时拼接字符串,而是在编译期约束字符串的格式。
大多数人对模板字面量类型的理解停留在"可以做 ${string} 匹配",但实际它有三个更重要的能力:联合展开、模式匹配(配合 infer)和大小写变换。
基础:模板字面量的联合展开
模板字面量类型被联合类型插值时,会自动展开为所有组合:
type Size = 'small' | 'medium' | 'large'
type Color = 'red' | 'blue'
type ButtonClass = `btn-${Size}-${Color}`
// "btn-small-red" | "btn-small-blue" | "btn-medium-red"
// | "btn-medium-blue" | "btn-large-red" | "btn-large-blue"
这看起来像字符串拼接,实际是笛卡尔积:每个插值位置的联合类型都会和其他位置做排列组合。3 × 2 = 6 个结果。如果每个位置都是宽类型(如 string),结果只会是 string——因为 string 不是联合类型,没有展开的行为。
更实用的场景是自动生成 CSS 类名或样式键:
type Spacing = '0' | '1' | '2' | '3' | '4'
type Side = 't' | 'b' | 'l' | 'r'
type MarginClass = `m${Side}-${Spacing}`
// "mt-0" | "mt-1" | "mt-2" | ... | "mb-0" | ...
模式匹配:模板字面量 + infer 的组合
模板字面量真正的价值是和 infer 配合做字符串模式匹配。它允许类型根据字符串格式推断出变量部分:
type ExtractVersion<T extends string> =
T extends `v${infer V}` ? V : never
type V1 = ExtractVersion<'v1.0.0'> // "1.0.0"
type V2 = ExtractVersion<'2.0.0'> // never
这种模式不只是字符串切片——它在类型层面做了解析。来看一个更复杂的情况:从 API 路径中提取参数名。
假设你的路由模式是 /users/:id/posts/:postId,你想从路径字符串中提取所有以 : 开头的参数名:
type ExtractRouteParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<Rest>
: T extends `${infer _Start}:${infer Param}`
? Param
: never
type Params = ExtractRouteParams<'/users/:id/posts/:postId'>
// "id" | "postId"
这个递归类型的机制:
- 从左侧查找
:模式,提取:后的参数名直到下一个/ - 递归处理剩余部分
- 当不再匹配
:模式时终止
实战 1:类型安全的 API 客户端
以下是一个利用模板字面量类型做类型安全 API 调用的模式:
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiPath<M extends Methods, P extends string> =
`${M} /api/${P}`
type GetUser = ApiPath<'GET', 'users/:id'>
// "GET /api/users/:id"
type CreateUser = ApiPath<'POST', 'users'>
// "POST /api/users"
配合函数重载,可以把 API 路径变成类型安全的调用:
type Routes = {
'GET /api/users': { response: User[] }
'GET /api/users/:id': { params: { id: string }; response: User }
'POST /api/users': { body: CreateUserInput; response: User }
}
type ExtractParams<T extends string> =
T extends `${string}:${infer P}/${string}`
? P | ExtractParams<T>
: T extends `${string}:${infer P}`
? P
: never
async function apiCall<R extends keyof Routes>(
route: R,
...args: ExtractParams<R> extends never
? []
: [params: Record<ExtractParams<R>, string>]
): Promise<Routes[R]['response']> {
// 实现...
}
// 使用
const users = await apiCall('GET /api/users')
const user = await apiCall('GET /api/users/:id', { id: '123' })
实战 2:事件名称的类型安全
在事件驱动的架构里,事件名称通常是字符串常量。模板字面量类型可以自动生成事件名组合:
type Domain = 'user' | 'order' | 'payment'
type Action = 'created' | 'updated' | 'deleted'
type EventName = `${Domain}:${Action}`
// "user:created" | "user:updated" | ... | "payment:deleted"
type EventMap = {
[K in EventName]: {
domain: K extends `${infer D}:${string}` ? D : never
action: K extends `${string}:${infer A}` ? A : never
timestamp: number
data: unknown
}
}
实战 3:CSS 属性值的类型校验
如果你写过样式相关的工具函数,知道 CSS 值很多是有固定格式的:
type CSSLength = `${number}${'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'}`
type CSSColor = `#${string}` | `rgb(${number},${number},${number})`
function setStyle(prop: string, value: CSSLength | CSSColor) {
// 只接受合法的长度和颜色值
}
setStyle('width', '100px') // ✅
setStyle('width', '50%') // ✅
setStyle('width', 'auto') // ❌ —— auto 不是合法长度
大小写变换工具类型
TypeScript 内置了四个大小写辅助类型,常和模板字面量一起用:
type EventName = 'user-login' | 'user-logout' | 'order-paid'
// 驼峰转换
type ToCamel<S extends string> =
S extends `${infer P}-${infer Q}`
? `${P}${Capitalize<ToCamel<Q>>}`
: S
type CamelEvents = ToCamel<EventName>
// "userLogin" | "userLogout" | "orderPaid"
// 事件监听器类型
type EventListenerMap = {
[K in EventName as `on${Capitalize<ToCamel<K>>}`]: () => void
}
// { onUserLogin: () => void; onUserLogout: () => void; ... }
Capitalize 是四个内置类型之一,其他三个是 Uncapitalize、Uppercase、Lowercase。这些只对 ASCII 字母有效,不影响非字母字符。
模板字面量的性能考量
模板字面量类型在联合展开时会产生组合爆炸。如果每个插值位置有 10 个联合成员,3 个插值位置就会产生 1000(10³)个类型实例。虽然 TypeScript 会缓存已实例化的类型,但 1000+ 规模的联合仍然会影响编辑器的自动补全速度。
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' // 5
type Color = 'red' | 'blue' | 'green' | 'yellow' // 4
type Variant = 'solid' | 'outline' | 'ghost' // 3
type AllCombos = `btn-${Size}-${Color}-${Variant}`
// 5 × 4 × 3 = 60 —— 可接受
// 如果要增加到更多维度,观察编辑器的响应速度
如果发现编辑器在补全联合类型时明显卡顿,说明类型实例化数量超过了阈值。考虑分拆成更小的联合或限制数量。
总结
模板字面量类型最有价值的三种用法:
| 用法 | 场景 | 示例 |
|---|---|---|
| 联合展开 | 生成类名、事件名、路径 | \btn-${Size}-${Color}`` |
| infer 模式匹配 | 提取路径参数、解析事件 | \${string}:${infer P}/${string}`` |
| 大小写变换 | 命名转换、事件适配 | Capitalize<ToCamel<K>> |
模板字面量类型的适用边界很清晰:当你的字符串有固定格式但内容可变时,用类型去约束它比运行时校验更早发现问题。但如果字符串格式太灵活,比如自然语言或自由文本,模板字面量类型就帮不上忙了——它只知道格式,不懂语义。


