模板字面量类型(TS 4.1+)把字符串拼接和模式匹配搬到了类型层面。它是"批量改写键名"和"解析字符串结构"的核心工具。
基础
1// 模板字面量类型的模式匹配
2
3// ---- 基础拼接 ----
4type EventName = "click" | "focus";
5type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus"
6
7// 联合会做笛卡尔积
8type Size = "sm" | "md";
9type Color = "red" | "blue";
10type Variant = `${Size}-${Color}`; // 4 个成员
11
12// ---- 四个内置字符串操作类型 ----
13type A = Uppercase<"abc">; // "ABC"
14type B = Lowercase<"ABC">; // "abc"
15type C = Capitalize<"abc">; // "Abc"
16type D = Uncapitalize<"Abc">; // "abc"
17
18// ---- infer 提取 ----
19// 提取路径剩余部分
20type StripLeadingSlash<T> = T extends `/${infer Rest}` ? Rest : never;
21type E = StripLeadingSlash<"/users/1">; // "users/1"
22
23// 只切第一段
24type SplitFirst<T> = T extends `${infer Head}/${infer Tail}` ? [Head, Tail] : never;
25type F = SplitFirst<"a/b/c">; // ["a", "b/c"]
26
27// 解析键值对
28type ParseKV<T> = T extends `${infer K}=${infer V}` ? { key: K; value: V } : never;
29type G = ParseKV<"name=rainboy">; // { key: "name"; value: "rainboy" }
30
31// 提取路径参数
32type PathParams<T> = T extends `${string}:${infer P}/${infer Rest}`
33 ? P | PathParams<`/${Rest}`>
34 : T extends `${string}:${infer P}`
35 ? P
36 : never;
37type H = PathParams<"/users/:id/posts/:postId">; // "id" | "postId"
38
39// ---- 递归转换 ----
40// 蛇形转驼峰
41type SnakeToCamel<S extends string> =
42 S extends `${infer Head}_${infer Tail}`
43 ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
44 : S;
45type I = SnakeToCamel<"user_first_name">; // "userFirstName"
46
47// 驼峰转蛇形
48type CamelToSnake<S extends string> =
49 S extends `${infer Head}${infer Tail}`
50 ? Head extends Uppercase<Head>
51 ? Head extends Lowercase<Head>
52 ? `${Head}${CamelToSnake<Tail>}`
53 : `_${Lowercase<Head>}${CamelToSnake<Tail>}`
54 : `${Head}${CamelToSnake<Tail>}`
55 : S;
56type J = CamelToSnake<"userFirstName">; // "user_first_name"
57
58// 替换字符串
59type Replace<S extends string, From extends string, To extends string> =
60 S extends `${infer Head}${From}${infer Tail}` ? `${Head}${To}${Tail}` : S;
61type K = Replace<"a-b-c", "-", "_">; // "a_b-c"(只替换第一处)
62
63// 全部替换(递归)
64type ReplaceAll<S extends string, From extends string, To extends string> =
65 S extends `${infer Head}${From}${infer Tail}`
66 ? `${Head}${To}${ReplaceAll<Tail, From, To>}`
67 : S;
68type L = ReplaceAll<"a-b-c", "-", "_">; // "a_b_c"
69
70// ---- 与映射类型组合:批量改键名 ----
71interface ApiResponse {
72 user_id: number;
73 user_name: string;
74 created_at: string;
75}
76type CamelKeys<T> = {
77 [P in keyof T as SnakeToCamel<string & P>]: T[P];
78};
79type M = CamelKeys<ApiResponse>;
80// { userId: number; userName: string; createdAt: string }
81
82// ---- 数字与 bigint ----
83type SemverMajor = `v${number}`;
84const v1: SemverMajor = "v1";
85const v1_5: SemverMajor = "v1.5";
86// @ts-expect-error TS2322: "v1.2.3" 不是单个 number 字面量
87const v1_2_3: SemverMajor = "v1.2.3";
88
89// 需要任意字符串时用 string
90type Version = `v${string}`;
91const vAny: Version = "v1.2.3";
92
93// 布尔也会被转成字符串
94type BoolStr = `${boolean}`; // "true" | "false"
95const bt: BoolStr = "true";
96
97// null / undefined
98type NullStr = `${null}`; // "null"
99const nt: NullStr = "null";
100
101// ---- 过滤:判断是否以某前缀开头 ----
102type StartsWith<T, P extends string> = T extends `${P}${string}` ? true : false;
103type N = StartsWith<"onClick", "on">; // true
104type O = StartsWith<"click", "on">; // false
105
106const a: A = "ABC";
107const b: B = "abc";
108const c: C = "Abc";
109const d: D = "abc";
110const e: E = "users/1";
111const f: F = ["a", "b/c"];
112const g: G = { key: "name", value: "rainboy" };
113const h: H = "id";
114const i: I = "userFirstName";
115const j: J = "user_first_name";
116const k: K = "a_b-c";
117const l: L = "a_b_c";
118const m: M = { userId: 1, userName: "a", createdAt: "d" };
119const variant: Variant = "sm-red";
120const n: N = true;
121const o: O = false;
122
123console.log(a, b, c, d, e, f, g, h, i, j, k, l, m, variant, v1, v1_5, vAny, bt, nt, n, o, v1_2_3);
1ABC abc Abc abc users/1 [ 'a', 'b/c' ] { key: 'name', value: 'rainboy' } id userFirstName user_first_name a_b-c a_b_c { userId: 1, userName: 'a', createdAt: 'd' } sm-red v1 v1.5 v1.2.3 true null true false v1.2.3
拼接语法和运行时模板字符串一样,只是位置在类型里:
1type EventName = "click" | "focus";
2type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus"
联合类型会做笛卡尔积:
1type Size = "sm" | "md";
2type Color = "red" | "blue";
3type Variant = `${Size}-${Color}`; // 4 个成员的联合
四个内置字符串操作类型
1Uppercase<"abc"> // "ABC"
2Lowercase<"ABC"> // "abc"
3Capitalize<"abc"> // "Abc"
4Uncapitalize<"Abc"> // "abc"
它们只作用于字面量字符串类型。传入 string 会得到 string。
用 infer 做模式匹配
1// 去掉前导斜杠
2type StripLeadingSlash<T> = T extends `/${infer Rest}` ? Rest : never;
3type E = StripLeadingSlash<"/users/1">; // "users/1"
4
5// 只切第一段
6type SplitFirst<T> = T extends `${infer Head}/${infer Tail}` ? [Head, Tail] : never;
7type F = SplitFirst<"a/b/c">; // ["a", "b/c"]
8
9// 解析键值对
10type ParseKV<T> = T extends `${infer K}=${infer V}` ? { key: K; value: V } : never;
11type G = ParseKV<"name=rainboy">; // { key: "name"; value: "rainboy" }
注意 SplitFirst<"a/b/c"> 的结果是 ["a", "b/c"]——非贪婪匹配,只切第一段。
递归转换
模板字面量类型可以递归,这是"字符串算法"在类型层面的实现:
1// 蛇形转驼峰
2type SnakeToCamel<S extends string> =
3 S extends `${infer Head}_${infer Tail}`
4 ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
5 : S;
6
7type I = SnakeToCamel<"user_first_name">; // "userFirstName"
全部替换需要递归(因为 infer 只匹配第一处):
1type Replace<S extends string, From extends string, To extends string> =
2 S extends `${infer Head}${From}${infer Tail}` ? `${Head}${To}${Tail}` : S;
3
4type K = Replace<"a-b-c", "-", "_">; // "a_b-c"(只替换第一处)
5
6type ReplaceAll<S extends string, From extends string, To extends string> =
7 S extends `${infer Head}${From}${infer Tail}`
8 ? `${Head}${To}${ReplaceAll<Tail, From, To>}`
9 : S;
10
11type L = ReplaceAll<"a-b-c", "-", "_">; // "a_b_c"
与映射类型组合:批量改键名
这是模板字面量类型最有价值的应用:
1interface ApiResponse {
2 user_id: number;
3 user_name: string;
4 created_at: string;
5}
6
7type CamelKeys<T> = {
8 [P in keyof T as SnakeToCamel<string & P>]: T[P];
9};
10
11type M = CamelKeys<ApiResponse>;
12// { userId: number; userName: string; createdAt: string }
string & P 是必需的——keyof T 的元素类型是 string | number | symbol,而模板字面量只接受字符串。详见 映射类型。
判断前缀/后缀
1type StartsWith<T, P extends string> = T extends `${P}${string}` ? true : false;
2
3type N = StartsWith<"onClick", "on">; // true
4type O = StartsWith<"click", "on">; // false
${number} 的边界
1type SemverMajor = `v${number}`;
2
3const v1: SemverMajor = "v1"; // ✓
4const v1_5: SemverMajor = "v1.5"; // ✓
5
6const v1_2_3: SemverMajor = "v1.2.3";
7// error TS2322: Type '"v1.2.3"' is not assignable to type '`v${number}`'.
${number} 只匹配单个合法的数字字面量。1.2.3 不是合法数字,所以不匹配。需要任意字符串时用 ${string}:
1type Version = `v${string}`;
2const vAny: Version = "v1.2.3"; // ✓
其他占位类型
| 占位 | 匹配 | 实测通过的示例 |
|---|---|---|
${string} | 任意字符串 | "anything" |
${number} | 合法数字字面量 | "1"、"1.5"、"-1"、"1e3"、"0x10" |
${bigint} | 整数字面量 | "1"、"1n" |
${boolean} | 布尔 | "true" | "false" |
${null} | null | "null" |
${undefined} | undefined | "undefined" |
实测确认 ${number} 的匹配范围比想象的宽——负数、科学计数法、十六进制都算合法数字字面量,都会通过。它只排除真正不合法的形式(如 "1.2.3"、"vabc"):
1error TS2322: Type '"v1.2.3"' is not assignable to type '`v${number}`'.
2error TS2322: Type '"vabc"' is not assignable to type '`v${number}`'.
实用模式
| 需求 | 写法 |
|---|---|
| 加前缀 | `get${Capitalize<K>}` |
| 去前缀 | K extends `${P}${infer R}` ? R : K |
| 全部替换 | 递归 |
| 判断前缀 | T extends `${P}${string}` |
| 解析键值 | T extends `${infer K}=${infer V}` |
相关
- 映射类型 ——
as子句里的键重映射 - 条件类型与 infer ——
infer的完整规则 - 高级类型 —— 模板字面量类型的基础