模板字面量类型
TS 4.1 引入。它是类型层的字符串拼接与匹配,让类型系统第一次能"处理字符串"。
基本拼接
ts
type World = 'world' | 'TS'
type Greeting = `hello ${World}`
联合会自动展开成笛卡尔积:
ts
type A = 'top' | 'bottom'
type B = 'left' | 'right'
type Position = `${A}-${B}`
内置字符串工具类型
四个编译器内置的类型(没有 TS 源码,是 intrinsic):
ts
type A = Uppercase<'abc'>
type B = Lowercase<'ABC'>
type C = Capitalize<'abc'>
type D = Uncapitalize<'Abc'>
模式匹配:${infer X}
这是它最有用的地方——能"解析"字符串。
ts
type SplitFirst<S extends string> = S extends `${infer A}.${infer B}` ? [A, B] : [S]
type X = SplitFirst<'user.name'>
type Y = SplitFirst<'username'>
匹配规则:${infer A} 是非贪婪的,匹配到第一个分隔符就停。
ts
type A = 'a-b-c' extends `${infer X}-${infer Y}` ? [X, Y] : never
递归处理字符串
类型层没有循环,处理任意长度的字符串只能递归:
ts
type ReplaceAll<
S extends string,
From extends string,
To extends string
> = From extends ''
? S
: S extends `${infer L}${From}${infer R}`
? `${L}${To}${ReplaceAll<R, From, To>}`
: S
type A = ReplaceAll<'a-b-c', '-', '/'>
要点:
- 先处理边界(空
From会无限递归) - 每次递归缩短输入(
R一定比S短) - 已经不含
From时落到 false 分支,递归结束
数字符串长度
ts
type StrLen<
S extends string,
Acc extends unknown[] = []
> = S extends `${infer _}${infer Rest}` ? StrLen<Rest, [...Acc, unknown]> : Acc['length']
type A = StrLen<'hello'>
用元组长度当计数器——这是类型层做算术的唯一手段,见递归与元组计数。
实用场景
1. 键名转换
ts
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}
interface Todo {
title: string
done: boolean
}
type G = Getters<Todo>
2. 事件名
ts
type EventName<T extends string> = `on${Capitalize<T>}`
type Handler<T extends string> = {
[K in EventName<T>]: (payload: unknown) => void
}
type H = Handler<'click' | 'change'>
3. 路径参数校验
ts
type Route = `/${string}`
const a: Route = '/home'4. CSS-in-JS 的单位
ts
type Size = `${number}px` | `${number}rem` | `${number}%`
const s1: Size = '16px'
const s2: Size = '50%'5. 模板字符串推导(4.3+)
ts
declare function makeId(prefix: string): `${string}-${number}`
const id = makeId('user')
type T = typeof id
函数返回值也可以声明成模板字面量类型。
限制
1. 联合展开会爆炸
ts
type Big = `${'a' | 'b' | 'c'}-${'x' | 'y' | 'z'}-${'1' | '2' | '3'}`结果是 27 个成员。再多就会触发 "Expression produces a union type that is too complex to represent"。
2. 不能做正则
${infer X} 只能做分隔符匹配,没有正则能力。要做复杂解析得手写递归状态机。
3. number 插值有范围限制
ts
type A = `${number}`
${number} 匹配任意数字字符串,但反向(从字符串转数字)需要用 T extends \${infer N extends number}`` 这类写法,且大数会有问题。
4. symbol 不能直接插值
ts
type S<K extends string | number> = `x_${K}`
type A = S<'a'>
symbol 要先过滤掉(Extract<keyof T, string>)。