前置知识: JavaScript

模板字面量类型

11 min高级

TypeScript 模板字面量类型(Template Literal Types)的形式化定义、字符串模式匹配规则、内置字符串操作类型、CSS 属性类型、路由参数类型推导、SQL 类型安全与运行时模板字符串的对比

前置知识

学习目标

  • 掌握「1. 学习导论」的核心机制、典型用法与常见陷阱
  • 掌握「2. 历史动机与技术演进」的核心机制、典型用法与常见陷阱
  • 掌握「3. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「4. 理论推导」的核心机制、典型用法与常见陷阱
  • 掌握「5. 内置字符串操作类型」的核心机制、典型用法与常见陷阱

阅读提示:正文以代码和白话为主,不出现类型论公式。进阶文档中若出现 Γ ⊢ e : τ 这类记号,第一遍可完全跳过(完整规则见 001-HowToReadThisCourse)。

1. 学习导论

1.1 为什么必须理解模板字面量类型

在 TypeScript 工程实践中,以下场景反复出现:

  1. 类型安全的字符串拼接:将 'click' | 'hover' | 'focus' 派生为 'onClick' | 'onHover' | 'onFocus',避免手写时遗漏。
  2. 路由参数解析:从 '/users/:userId/posts/:postId' 自动推导 { userId: string; postId: string },而非手动维护接口。
  3. CSS 属性类型:将 'margin' | 'padding' 与 'top' | 'right' | 'bottom' | 'left' 笛卡尔积为 'margin-top' | 'margin-right' | ...,并保证类型补全。
  4. SQL 列类型映射:从 SELECT id, name FROM users 推导 { id: number; name: string },让 ORM 具备类型推断能力。
  5. JSON Schema 派生:将 'required' | 'optional' 与字段名组合为 'required:name' | 'optional:age',用于元编程。

模板字面量类型是上述所有场景的共同基础。它将 ES2015 模板字符串从运行时提升至类型层,使 TypeScript 拥有了字符串级别的元编程能力。理解它意味着能回答以下问题:

  • 当 TypeScript 看到 `hello ${World}` 时,它如何求值?
  • 当占位符内是联合类型时,结果是什么?
  • 当模式匹配 ${infer Head}${infer Tail} 中 Head 与 Tail 同时可匹配时,TypeScript 如何选择?
  • 递归模板字面量类型在多少层后会触发 “Type instantiation is excessively deep”?
  • 与运行时模板字符串相比,模板字面量类型在 Unicode 处理、性能、可读性上有何差异?

1.2 Bloom 认知层次对照

Bloom 层次对应能力本文对应章节
remember记住语法与四个内置操作类型第 3、5 节
understand理解模式匹配规则与历史动机第 2、6 节
apply实现 CSS/路由/事件类型派生第 8、9、11 节
analyze分析递归深度与求值策略第 4、6 节
evaluate评估性能与可维护性第 13、14 节
create设计 SQL 类型安全查询器第 10、15 节

1.3 阅读建议

  • 入门读者:先读第 2、3、5、6 节,建立直觉,再跳到第 8、9 节看应用。
  • 工程实践者:直接跳到第 9、10、13、14 节,对照生产问题。
  • 类型论研究者:精读第 3、4 节,对照 Hosoya-Pierce 2003 的正则表达式类型论文。

2. 历史动机与技术演进

2.1 ES2015 模板字符串回顾

ES2015(ES6)在 2015 年 6 月引入了模板字符串(template strings),由 Dave Herman 等人在 TC39 提案。其语法为反引号包裹的字符串,允许通过 ${expression} 嵌入任意表达式:

// 运行时模板字符串
const name = 'world';
const greeting = `hello ${name}`; // 'hello world'

const x = 1, y = 2;
const sum = `${x} + ${y} = ${x + y}`; // '1 + 2 = 3'

// 多行字符串
const html = `
  <div>
    <p>${name}</p>
  </div>
`;

// 带标签的模板字符串
function tag(strings, ...values) {
  return strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), '');
}
const tagged = tag`Hello ${name}!`; // 'Hello world!'

模板字符串解决了三个痛点:字符串拼接冗长、多行字符串需用 \n 转义、拼接逻辑与字符串内容耦合。但它本质上仍是运行时机制,TypeScript 在 2015 年仅能将其推断为 string,无法捕获字面量信息。

2.2 字面量类型的引入(TypeScript 1.8)

TypeScript 1.8(2016 年 2 月)引入了字符串字面量类型,允许将字符串值作为类型使用:

type Color = 'red' | 'green' | 'blue';
const c: Color = 'red'; // OK
const d: Color = 'yellow'; // Error

这是迈向类型层字符串操作的第一步,但仍然只能静态声明,无法从已有字面量派生新字面量。

2.3 模板字面量类型的诞生(TypeScript 4.1)

2020 年 11 月 19 日,TypeScript 4.1 正式发布,引入了模板字面量类型(Template Literal Types)。该版本同时引入了映射类型的键重映射(key remapping via as)与递归条件类型(recursive conditional types),三者共同构成了 TypeScript 类型层元编程的基石。

官方发布说明中,Daniel Rosenwasser 写道:

模板字面量类型在社区中得到了非常热烈的响应。它们使得 TypeScript 能够表达以前只能通过运行时校验才能保证的类型约束,将类型安全性扩展到了字符串层面。

模板字面量类型的设计目标包括:

  1. 类型层字符串拼接:将已有字面量类型组合为新字面量,无需手写。
  2. 类型层模式匹配:通过 infer 提取字符串的组成部分,实现解析器。
  3. 键重映射:与映射类型结合,根据旧键派生新键,实现类型层的对象变换。
  4. 字符串操作类型:提供 Uppercase/Lowercase/Capitalize/Uncapitalize 四个内置操作。

2.4 TypeScript 演进时间线

时间版本关键特性
2015-06ES2015运行时模板字符串
2016-02TypeScript 1.8字符串字面量类型
2018-06TypeScript 2.6strictFunctionTypes(与本文间接相关)
2019-11TypeScript 3.7条件类型 infer 改进
2020-11TypeScript 4.1模板字面量类型、键重映射、递归条件类型
2021-08TypeScript 4.4改进模板字面量类型的归一化
2022-03TypeScript 4.7改进 extends string 推断
2022-09TypeScript 4.9satisfies 操作符(与模板字面量类型配合)
2023-03TypeScript 5.0const 类型参数(增强字面量推断)
2024-03TypeScript 5.4改进 NoInfer 与模板字面量类型交互
2024-11TypeScript 5.7改进模板字面量类型在联合类型上的展开性能

2.5 关键设计者

  • Daniel Rosenwasser:TypeScript 项目主管,主导模板字面量类型的设计与发布说明。
  • Gabriel Bierman:Microsoft Research Cambridge,TypeScript 语义奠基者,2014 ECOOP 论文《Understanding TypeScript》作者,为模板字面量类型提供了语义模型。
  • Andrew Branch:TypeScript 团队成员,负责模板字面量类型与模块解析的工程实现。
  • Pierce, B. C.:宾夕法尼亚大学教授,类型论权威,其与 Hosoya 合作的 2003 年正则表达式类型论文为模板字面量类型提供了理论基础。
  • Laura M. P.:DefinitelyTyped 维护者,推动了 @types 中模板字面量类型的早期采用。

2.6 学术渊源:正则表达式类型

模板字面量类型并非凭空诞生。Hosoya 与 Pierce 在 2003 年发表于 ACM TOPLAS 的论文《Regular expression types for XML》提出了正则表达式类型(regular expression types),用于描述 XML 文档的结构。该论文将字符串类型与正则表达式结合,使类型系统能表达 String matching /a*b/ 这样的约束。

TypeScript 模板字面量类型可视为正则表达式类型的一个受限子集:

特性正则表达式类型(Hosoya-Pierce)TypeScript 模板字面量类型
字面量匹配支持支持
字符类支持 [a-z]不直接支持,需用联合类型
量词 * + ?支持通过递归实现
捕获组支持通过 infer 实现
反向引用支持不支持
通配符 .支持通过 ${string} 实现
求值复杂度PSPACE-completeO(n)O(n) 至 O(n2)O(n^2)

TypeScript 的限制是工程妥协:放弃反向引用与字符类,换取编译期的可判定性与合理性能。


3. 形式化定义

3.1 语法

模板字面量类型的语法为反引号包裹的字符串,其中 ${T} 为类型占位符:

TemplateLiteral  ::=  ‘ ( Chars∣$${ T } )∗ ‘\text{TemplateLiteral} \; ::= \; \text{`}\, (\,\text{Chars} \mid \$\$\{ \, T \, \}\,)^{*}\, \text{`}

其中 TT 必须可赋值给 string | number | bigint | boolean | null | undefined,但实际只有 string 与 string 的联合类型会产生有意义的结果。

3.2 求值规则

设 T1,T2,…,TnT_1, T_2, \dots, T_n 为占位符类型,s0,s1,…,sns_0, s_1, \dots, s_n 为字面量字符序列。模板字面量类型的求值规则定义为:

Eval(‘s0$T1s1$T2…$Tnsn‘)=⋃i=1n{s0⋅v1⋅s1⋅v2⋅⋯⋅vn⋅sn∣vi∈Literals(Ti)}\text{Eval}\left( \text{`} s_0 \${T_1} s_1 \${T_2} \dots \${T_n} s_n \text{`} \right) = \bigcup_{i=1}^{n} \left\{ s_0 \cdot v_1 \cdot s_1 \cdot v_2 \cdot \dots \cdot v_n \cdot s_n \mid v_i \in \text{Literals}(T_i) \right\}

其中 Literals(T)\text{Literals}(T) 将类型 TT 展开为字面量字符串集合:

  • Literals(’a’)={’a’}\text{Literals}(\text{'a'}) = \{ \text{'a'} \}
  • Literals(’a’∣’b’)={’a’,’b’}\text{Literals}(\text{'a'} \mid \text{'b'}) = \{ \text{'a'}, \text{'b'} \}
  • Literals(string)=string\text{Literals}(\text{string}) = \text{string}(特殊:保留为 string 类型)
  • Literals(number)=string\text{Literals}(\text{number}) = \text{string}(数字被转换为字符串)
  • \text{Literals}(\text{`x{T}`}) = { \text{‘x’} \cdot v \mid v \in \text{Literals}(T) }$(递归)

3.3 联合分发

当占位符为联合类型 T=T1∣T2∣⋯∣TnT = T_1 \mid T_2 \mid \dots \mid T_n 时,模板字面量类型对联合进行笛卡尔积展开:

\text{Eval}(\text{`prefix-${T}`}) = \text{`prefix-${T_1}`} \mid \text{`prefix-${T_2}`} \mid \dots \mid \text{`prefix-${T_n}`}

多占位符时进行笛卡尔积:

Eval(‘A−B‘)=⋃a∈Literals(A)⋃b∈Literals(B){‘a−b‘}\text{Eval}(\text{`${A}-${B}`} ) = \bigcup_{a \in \text{Literals}(A)} \bigcup_{b \in \text{Literals}(B)} \{ \text{`${a}-${b}`} \}

3.4 类型层与运行时的一致性

模板字面量类型与运行时模板字符串在拼接语义上一致,但有以下关键差异:

维度类型层运行时
求值时机编译期运行时
输入字面量类型与联合任意值
输出字面量类型或 stringstring
联合处理笛卡尔积展开取实际值
性能影响编译时间影响运行时间
UnicodeASCII 大小写映射完整 Unicode 映射
locale不支持支持

3.5 TypeScript 中各位置的型变归类

模板字面量类型本身是协变的构造子:

S <: T \Rightarrow \text{`x${S}`} <: \text{`x${T}`}

但因字面量类型的离散性,实际表现为:

  • 'a' <: string,则 `x${'a'}` <: `x${string}`,但 `x${string}` 归一化为 string。

4. 理论推导

4.1 求值的代数结构

模板字面量类型在字符串字面量类型上构成一个自由幺半群(free monoid):

(Str,⋅,ϵ)(\text{Str}, \cdot, \epsilon)

其中 Str\text{Str} 为字符串字面量类型的集合,⋅\cdot 为字符串拼接(由模板字面量类型实现),ϵ\epsilon 为空字符串 ''。

满足公理:

  • 结合律:(A⋅B)⋅C=A⋅(B⋅C)(A \cdot B) \cdot C = A \cdot (B \cdot C),即 `${`${A}${B}`}${C}` = `${A}${`${B}${C}`}`
  • 单位元:A⋅ϵ=A=ϵ⋅AA \cdot \epsilon = A = \epsilon \cdot A,即 `${A}${''}` = `${''}${A}` = A

4.2 模式匹配的代数语义

infer 模式匹配对应于字符串的前缀分解(prefix decomposition):

Match(S,‘PR‘)={{(p,r)∣S=p⋅r,p∈Literals(P),r∈Literals(R)}若存在∅否则\text{Match}(S, \text{`${P}${R}`}) = \begin{cases} \{ (p, r) \mid S = p \cdot r, p \in \text{Literals}(P), r \in \text{Literals}(R) \} & \text{若存在} \\ \emptyset & \text{否则} \end{cases}

TypeScript 选择最短匹配(shortest match)策略:当 Head 与 Tail 同时可匹配时,Head 优先匹配最短前缀。这对应正则表达式的非贪婪(lazy)量词。

4.3 求值的复杂度

TypeScript 模板字面量类型的求值复杂度分析:

操作复杂度说明
单字面量拼接O(1)O(1)直接拼接
联合分发$O(\prodT_i
模式匹配O(n)O(n)单次匹配
递归模式匹配O(n⋅d)O(n \cdot d)nn 为字符串长度,dd 为递归深度
多个 inferO(2n)O(2^n)在最坏情况下

TypeScript 5.7 引入了对联合展开的优化,使大联合的求值从指数级降为线性。

4.4 终止性与可判定性

递归模板字面量类型的终止性由以下机制保证:

  1. 类型实例化深度上限:默认约 50 层展开,超出报 “Type instantiation is excessively deep”。
  2. 尾递归优化(TypeScript 4.5+):对尾位置的递归调用进行优化,避免栈增长。
  3. 递归类型复杂度上限:默认约 1000 次展开。

形式化地,递归模板字面量类型的求值可建模为:

Evald(T)={⊥若 d>Dmax⁡展开(T,Evald+1)否则\text{Eval}_d(T) = \begin{cases} \bot & \text{若 } d > D_{\max} \\ \text{展开}(T, \text{Eval}_{d+1}) & \text{否则} \end{cases}

其中 Dmax⁡≈50D_{\max} \approx 50 为深度上限。

4.5 Liskov 行为子类型约束

模板字面量类型的派生类型满足 Liskov 行为子类型约束:

\text{若 } S <: T, \text{则 } \text{`prefix-${S}`} <: \text{`prefix-${T}`}

这意味着:接受 `prefix-${T}` 的函数也能接受 `prefix-${S}`,符合替换原则。


5. 内置字符串操作类型

5.1 四个内置操作

TypeScript 4.1 同时引入了四个内置字符串操作类型:

type A = Uppercase<'hello'>;      // 'HELLO'
type B = Lowercase<'HELLO'>;      // 'hello'
type C = Capitalize<'hello'>;     // 'Hello'
type D = Uncapitalize<'Hello'>;   // 'hello'

5.2 语义定义

四个操作类型的语义基于 ECMAScript 抽象操作:

类型对应抽象操作实现语义
Uppercase<S>StringToUpperCase每个字符转为大写
Lowercase<S>StringToLowerCase每个字符转为小写
Capitalize<S>StringToCodePoints + 首字符 StringToUpperCase首字符大写,其余不变
Uncapitalize<S>StringToCodePoints + 首字符 StringToLowerCase首字符小写,其余不变

5.3 与运行时 toUpperCase 的差异

关键差异在于 Unicode 处理:

// 类型层(简单 ASCII 大小写映射)
type T1 = Uppercase<'istanbul'>; // 'ISTANBUL'

// 运行时(locale-sensitive)
'turkey'.toLocaleUpperCase('tr-TR'); // 'TURKİY'(注意 İ)

// 类型层不支持 locale
type T2 = Uppercase<'istanbul'>; // 'ISTANBUL'(与 locale 无关)

ES2024 规范的 StringToUpperCase 抽象操作使用 Unicode Default Case Folding,但 TypeScript 的 Uppercase<S> 类型在编译期使用简化映射,不处理 locale-sensitive 字符。这意味着:

  • 'i' → 'I'(始终)
  • 'ß' → 'SS'(在 ECMAScript 中是标准映射,但 TypeScript 的类型层可能不完整)

5.4 在联合类型上的分发

四个操作类型对联合类型自动分发:

type Events = 'click' | 'hover' | 'focus';
type Handlers = `on${Capitalize<Events>}`;
// 'onClick' | 'onHover' | 'onFocus'

type Status = 'pending' | 'success' | 'error';
type Constants = Uppercase<Status>;
// 'PENDING' | 'SUCCESS' | 'ERROR'

5.5 与 string 类型的交互

当输入为 string 而非字面量时,操作类型返回 string:

type T1 = Uppercase<string>; // string
type T2 = Capitalize<string>; // string
type T3 = Lowercase<any>; // string

但模板字面量类型保留了字面量信息:

type T1 = `value: ${string}`; // `value: ${string}`(保留模板字面量)
type T2 = `value: ${number}`; // `value: ${number}`

function format<T extends string | number>(v: T): `value: ${T}` {
  return `value: ${v}` as `value: ${T}`;
}

const x = format('hello'); // 'value: hello'
const y = format(42);      // 'value: 42'

5.6 应用:事件处理器派生

type EventName<T extends string> = `on${Capitalize<T>}`;
type EventHandler<E extends string, Payload> = (event: E, payload: Payload) => void;
type EventMap<Events extends string, Payload> = {
  [E in Events as EventName<E>]: EventHandler<E, Payload>;
};

type ClickEvents = 'click' | 'dblclick' | 'mousedown';
type ClickHandlers = EventMap<ClickEvents, MouseEvent>;
// {
//   onClick: (event: 'click', payload: MouseEvent) => void;
//   onDblclick: (event: 'dblclick', payload: MouseEvent) => void;
//   onMousedown: (event: 'mousedown', payload: MouseEvent) => void;
// }

6. 字符串模式匹配

6.1 基础模式匹配

通过 infer 关键字,模板字面量类型可实现字符串的模式匹配:

// 提取前缀
type RemovePrefix<S extends string, P extends string> =
  S extends `${P}${infer Rest}` ? Rest : S;

type A = RemovePrefix<'getUserById', 'get'>; // 'UserById'
type B = RemovePrefix<'setUserName', 'set'>; // 'UserName'
type C = RemovePrefix<'deleteUser', 'get'>;  // 'deleteUser'(无匹配)

// 提取后缀
type RemoveSuffix<S extends string, S2 extends string> =
  S extends `${infer Rest}${S2}` ? Rest : S;

type D = RemoveSuffix<'userName.ts', '.ts'>; // 'userName'
type E = RemoveSuffix<'config.json', '.json'>; // 'config'

6.2 多占位符模式

// 替换字符串中的子串
type Replace<S extends string, From extends string, To extends string> =
  S extends `${infer Start}${From}${infer End}`
    ? `${Start}${To}${End}`
    : S;

type R1 = Replace<'hello world', 'world', 'TypeScript'>; // 'hello TypeScript'
type R2 = Replace<'a-b-c', '-', '/'>; // 'a/b-c'(仅替换第一个)

// 全部替换(递归)
type ReplaceAll<S extends string, From extends string, To extends string> =
  S extends `${infer Start}${From}${infer End}`
    ? `${Start}${To}${ReplaceAll<End, From, To>}`
    : S;

type R3 = ReplaceAll<'a-b-c-d', '-', '/'>; // 'a/b/c/d'
type R4 = ReplaceAll<'hello.world.test', '.', '-'>; // 'hello-world-test'

6.3 贪婪与非贪婪匹配

TypeScript 模板字面量类型的 infer 采用非贪婪策略,即优先匹配最短前缀:

type Split<S extends string, Delim extends string> =
  S extends `${infer Head}${Delim}${infer Tail}`
    ? [Head, ...Split<Tail, Delim>]
    : [S];

type S1 = Split<'a-b-c-d', '-'>; // ['a', 'b', 'c', 'd']

// 关键观察:在 `${infer Head}${Delim}${infer Tail}` 中
// Head 匹配最短前缀(非贪婪),Tail 匹配剩余部分

与之对比,正则表达式默认贪婪,需用 ? 启用非贪婪:

// JavaScript 正则
'a-b-c-d'.match(/^(.+)-(.+)$/);  // ['a-b-c', 'd'],贪婪
'a-b-c-d'.match(/^(.+?)-(.+)$/); // ['a', 'b-c-d'],非贪婪

TypeScript 模板字面量类型的 infer 始终非贪婪,无需显式标注。

6.4 多个 infer 的求值顺序

当模式中有多个 infer 时,TypeScript 按从左到右的顺序求值:

type Parse<S extends string> =
  S extends `${infer Method} ${infer URL} HTTP/${infer Version}`
    ? { method: Method; url: URL; version: Version }
    : never;

type P1 = Parse<'GET /users HTTP/1.1'>;
// { method: 'GET'; url: '/users'; version: '1.1' }

求值过程:

  1. Method 非贪婪匹配到第一个空格前的 'GET'。
  2. URL 非贪婪匹配到下一个空格前的 '/users'。
  3. Version 匹配剩余的 '1.1'。

6.5 联合类型在模式匹配中的行为

当 infer 出现在联合类型的上下文中时,TypeScript 会进行分发:

type Getters<T> = {
  [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

type UserGetters = Getters<User>;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
// }

6.6 递归模式匹配

模板字面量类型可递归调用,实现复杂解析器:

// 字符串转字符数组
type StringToChars<S extends string> =
  S extends `${infer C}${infer Rest}` ? [C, ...StringToChars<Rest>] : [];

type Chars = StringToChars<'hello'>; // ['h', 'e', 'l', 'l', 'o']

// 反转字符串
type Reverse<S extends string> =
  S extends `${infer C}${infer Rest}` ? `${Reverse<Rest>}${C}` : S;

type R = Reverse<'hello'>; // 'olleh'

6.7 尾递归优化

TypeScript 4.5+ 对尾位置的递归调用进行优化,避免栈增长:

// 非尾递归(栈增长,易触发深度错误)
type JoinNonTail<S extends string[]> =
  S extends [infer First extends string, ...infer Rest extends string[]]
    ? `${First}${JoinNonTail<Rest>}`
    : '';

// 尾递归(TypeScript 4.5+ 优化)
type JoinTail<S extends string[], Acc extends string = ''> =
  S extends [infer First extends string, ...infer Rest extends string[]]
    ? JoinTail<Rest, `${Acc}${First}`>
    : Acc;

type J1 = JoinTail<['a', 'b', 'c', 'd']>; // 'abcd'

尾递归识别规则:

  • 递归调用必须出现在条件类型的直接分支位置(T extends X ? Recurse : Y)。
  • 不能被其他类型构造包裹(如 ${Recurse} 会破坏尾位置)。

7. 与映射类型结合

7.1 键重映射(Key Remapping)

TypeScript 4.1 引入了映射类型的键重映射语法:

type MappedType<T> = {
  [K in keyof T as NewKey<K>]: ValueType<T[K]>;
};

其中 as NewKey<K> 部分使用模板字面量类型对键进行变换。

7.2 基础示例:派生 getter

type Getters<T> = {
  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

7.3 派生 setter

type Setters<T> = {
  [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
};

type PersonSetters = Setters<Person>;
// {
//   setName: (value: string) => void;
//   setAge: (value: number) => void;
// }

7.4 过滤键

通过 as 子句返回 never,可过滤掉某些键:

type StringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type OnlyStrings = StringKeys<Mixed>;
// {
//   name: string;
//   email: string;
// }

7.5 添加前缀

type Prefixed<T, P extends string> = {
  [K in keyof T & string as `${P}${Capitalize<K>}`]: T[K];
};

interface Config {
  host: string;
  port: number;
}

type PrefixedConfig = Prefixed<Config, 'config'>;
// {
//   configHost: string;
//   configPort: number;
// }

7.6 派生可选版本

type Optional<T> = {
  [K in keyof T as `${K & string}?`]?: T[K];
};

type OptionalPerson = Optional<Person>;
// {
//   'name?'?: string;
//   'age?'?: number;
// }

7.7 完整的响应式对象派生

type Reactive<T> = {
  [K in keyof T & string as `${K}$`]: T[K] extends object
    ? Reactive<T[K]>
    : import('vue').Ref<T[K]>;
};

interface AppState {
  user: { name: string; age: number };
  count: number;
}

type ReactiveAppState = Reactive<AppState>;
// {
//   user$: Reactive<{ name: string; age: number }>;
//   count$: Ref<number>;
// }

8. CSS 属性类型系统

8.1 CSS 简写属性派生

type CSSProperty = 'margin' | 'padding' | 'border';
type Direction = 'top' | 'right' | 'bottom' | 'left';
type CSSShorthand = `${CSSProperty}-${Direction}`;
// 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'
// | 'padding-top' | ... | 'border-bottom' | 'border-left'

8.2 类型安全的 CSS 对象

type CSSProperties = {
  [K in CSSShorthand]: string | number;
};

const styles: CSSProperties = {
  'margin-top': 10,
  'padding-left': '20px',
  'border-bottom': '1px solid #ccc',
  // 'margin-center': 5,  // Error: 不在 CSSShorthand 中
};

8.3 主题色类型

type ColorShade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
type ThemeColor = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
type ColorToken = `${ThemeColor}-${ColorShade}`;
// 'primary-50' | 'primary-100' | ... | 'danger-900'

type Theme = {
  [K in ColorToken]: string;
};

const theme: Theme = {
  'primary-50': '#eff6ff',
  'primary-100': '#dbeafe',
  'primary-500': '#3b82f6',
  // ... 共 50 个
};

8.4 完整的 CSS-in-JS 类型系统

type CSSValue = string | number;
type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw';

type WithUnit<V extends CSSValue, U extends CSSUnit> = `${V}${U}`;
type Spacing = WithUnit<number, CSSUnit>;

type CSSProperties = {
  // 颜色属性
  color?: string;
  backgroundColor?: string;
  borderColor?: string;
  // 间距属性
  margin?: Spacing | 'auto';
  padding?: Spacing;
  // 尺寸属性
  width?: Spacing | 'auto' | '100%';
  height?: Spacing | 'auto' | '100%';
  // 边框属性
  border?: `${number}${CSSUnit} ${'solid' | 'dashed' | 'dotted'} ${string}`;
  borderRadius?: Spacing;
  // 字体属性
  fontSize?: Spacing;
  fontWeight?: 400 | 500 | 600 | 700;
  // 布局属性
  display?: 'block' | 'inline' | 'flex' | 'grid' | 'none';
  position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
};

const styles: CSSProperties = {
  margin: '10px',
  padding: 20,
  border: '1px solid #ccc',
  fontWeight: 600,
  display: 'flex',
  // border: '1 solid';  // Error: 缺少单位
  // fontWeight: 550;    // Error: 不在联合中
};

8.5 Tailwind 类名派生

type TailwindPrefix = 'm' | 'p' | 'w' | 'h' | 'text' | 'bg' | 'border';
type TailwindSize = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24;
type TailwindClass = `${TailwindPrefix}-${TailwindSize}`;
// 'm-0' | 'm-1' | ... | 'text-24' | 'bg-0' | ...

type ClassName = TailwindClass | `${TailwindClass} ${TailwindClass}`;

const cls: ClassName = 'm-4 p-2';
// const invalid: ClassName = 'm-7';  // Error: 7 不在 TailwindSize 中

9. 路由参数类型推导

9.1 基础路由参数解析

type Routes = '/users' | '/users/:id' | '/posts/:postId/comments' | '/posts/:postId/comments/:commentId';

type Params<T extends string> =
  T extends `${string}:${infer P}/${infer Rest}`
    ? { [K in P | keyof Params<Rest>]: string }
    : T extends `${string}:${infer P}`
      ? { [K in P]: string }
      : {};

type R1 = Params<'/users'>;                          // {}
type R2 = Params<'/users/:id'>;                      // { id: string }
type R3 = Params<'/posts/:postId/comments'>;         // { postId: string }
type R4 = Params<'/posts/:postId/comments/:commentId'>;
// { postId: string; commentId: string }

9.2 解析过程详解

对于路由 /posts/:postId/comments/:commentId:

  1. 第一条分支匹配 ${string}:${P}/${Rest},其中:
    • ${string} 匹配 '/posts/'
    • :${P} 匹配 ':postId',P = 'postId'
    • /${Rest} 匹配 '/comments/:commentId'
  2. 对 Rest = '/comments/:commentId' 递归调用 Params:
    • 第一条分支再次匹配,${string} = '/comments/',P = 'commentId',Rest = ''(空字符串)
    • 对 Rest = '' 递归:第二条分支不匹配,第三条分支返回 {}
  3. 合并结果:{ postId: string; commentId: string }

9.3 类型安全的路由函数

type RouteParams<T extends string> =
  T extends `${string}:${infer P}/${infer Rest}`
    ? { [K in P | keyof RouteParams<Rest>]: string }
    : T extends `${string}:${infer P}`
      ? { [K in P]: string }
      : {};

function navigate<T extends string>(
  route: T,
  params: RouteParams<T>
): string {
  let result: string = route;
  for (const key in params) {
    result = result.replace(`:${key}`, params[key]);
  }
  return result;
}

// 类型安全调用
const url1 = navigate('/users/:id', { id: '123' });              // OK
const url2 = navigate('/users/:id', {});                          // Error: 缺少 id
const url3 = navigate('/users/:id', { id: '123', extra: 'x' });   // Error: 多余属性
const url4 = navigate('/posts/:postId/comments/:commentId', {
  postId: '1',
  commentId: '2',
}); // OK

9.4 React Router 风格的路由表

type RouteTable = {
  '/': {};
  '/users': {};
  '/users/:id': { id: string };
  '/users/:userId/posts/:postId': { userId: string; postId: string };
  '/posts': {};
  '/posts/:postId': { postId: string };
};

type RoutePath = keyof RouteTable;

function push<T extends RoutePath>(
  path: T,
  ...args: keyof RouteTable[T] extends never ? [] : [RouteTable[T]]
): void {
  // 实现...
}

push('/');                              // OK,无参数
push('/users');                          // OK,无参数
push('/users/:id', { id: '123' });       // OK
push('/users/:id');                       // Error: 缺少参数
push('/users/:id', { wrong: 'x' });       // Error: 错误的参数

9.5 Vue Router 风格的命名路由

type NamedRoutes = {
  home: '/';
  user: '/users/:id';
  post: '/posts/:postId';
  comment: '/posts/:postId/comments/:commentId';
};

type ParamsOf<T extends string> =
  T extends `${string}:${infer P}/${infer Rest}`
    ? { [K in P | keyof ParamsOf<Rest>]: string }
    : T extends `${string}:${infer P}`
      ? { [K in P]: string }
      : {};

type RouteConfig = {
  [K in keyof NamedRoutes]: {
    path: NamedRoutes[K];
    params: ParamsOf<NamedRoutes[K]>;
  };
};

const routes: RouteConfig = {
  home: { path: '/', params: {} },
  user: { path: '/users/:id', params: { id: 'placeholder' } },
  post: { path: '/posts/:postId', params: { postId: 'placeholder' } },
  comment: {
    path: '/posts/:postId/comments/:commentId',
    params: { postId: 'p', commentId: 'c' },
  },
};

10. SQL 类型安全

10.1 SELECT 列类型推导

type Schema = {
  users: { id: number; name: string; age: number; email: string };
  posts: { id: number; title: string; content: string; authorId: number };
  comments: { id: number; text: string; postId: number };
};

// 提取 SELECT 子句中的列名
type ExtractColumns<S extends string> =
  S extends `SELECT ${infer Cols} FROM ${infer Table} ${string}`
    ? { table: Table; columns: SplitColumns<Trim<Cols>> }
    : S extends `SELECT ${infer Cols} FROM ${infer Table}`
      ? { table: Table; columns: SplitColumns<Trim<Cols>> }
      : never;

type SplitColumns<S extends string, Acc extends string[] = []> =
  S extends `${infer First}, ${infer Rest}`
    ? SplitColumns<Rest, [...Acc, Trim<First>]>
    : [...Acc, Trim<S>];

type Trim<S extends string> =
  S extends ` ${infer T}` ? Trim<T>
  : S extends `${infer T} ` ? Trim<T>
  : S;

// 根据列名推导结果类型
type QueryResult<S extends string, DB> =
  ExtractColumns<S> extends { table: infer T; columns: infer C }
    ? T extends keyof DB
      ? C extends string[]
        ? { [K in C[number] & keyof DB[T]]: DB[T][K] }
        : never
      : never
    : never;

type Q1 = QueryResult<'SELECT id, name FROM users', Schema>;
// { id: number; name: string }

type Q2 = QueryResult<'SELECT id, title, content FROM posts', Schema>;
// { id: number; title: string; content: string }

10.2 WHERE 子句参数

// 提取 WHERE 子句中的占位符
type ExtractPlaceholders<S extends string> =
  S extends `${string}WHERE ${infer Where}`
    ? ExtractPlaceholdersRec<Where>
    : [];

type ExtractPlaceholdersRec<S extends string, Acc extends string[] = []> =
  S extends `${infer Before}$${infer Num}${infer After}`
    ? ExtractPlaceholdersRec<After, [...Acc, Num]>
    : Acc;

type P1 = ExtractPlaceholders<'SELECT * FROM users WHERE id = $1'>; // ['1']
type P2 = ExtractPlaceholders<'SELECT * FROM users WHERE id = $1 AND name = $2'>; // ['1', '2']

10.3 类型安全的查询函数

type ParamsTuple<Placeholders extends string[]> =
  { [K in keyof Placeholders]: string | number };

function query<S extends string>(
  sql: S,
  ...params: ParamsTuple<ExtractPlaceholders<S>>
): Promise<QueryResult<S, Schema>> {
  // 实现省略
  return Promise.resolve({}) as Promise<QueryResult<S, Schema>>;
}

// 类型安全调用
const r1 = await query('SELECT id, name FROM users WHERE id = $1', 123);
// r1: { id: number; name: string }

const r2 = await query('SELECT * FROM users WHERE name = $1 AND age = $2', 'Alice', 30);
// r2: { id: number; name: string; age: number; email: string }

// const r3 = await query('SELECT id FROM users WHERE id = $1');  // Error: 缺少参数
// const r4 = await query('SELECT id FROM users WHERE id = $1', 1, 2);  // Error: 多余参数

10.4 INSERT 语句类型推导

type ExtractTable<S extends string> =
  S extends `INSERT INTO ${infer Table} ${string}` ? Table : never;

type ExtractColumnsFromInsert<S extends string> =
  S extends `INSERT INTO ${string} (${infer Cols}) ${string}`
    ? SplitColumns<Trim<Cols>>
    : never;

type InsertResult<S extends string, DB> =
  ExtractTable<S> extends infer T
    ? T extends keyof DB
      ? { [K in ExtractColumnsFromInsert<S>[number] & keyof DB[T]]: DB[T][K] }
      : never
    : never;

type I1 = InsertResult<'INSERT INTO users (id, name, age) VALUES ($1, $2, $3)', Schema>;
// { id: number; name: string; age: number }

10.5 局限性

模板字面量类型的 SQL 类型推导有以下局限:

  1. 无法处理动态拼接:SQL 必须为字面量字符串,query(SELECT ${cols} FROM users) 无法推导。
  2. 无法解析子查询:SELECT * FROM (SELECT ...) 的解析器实现极复杂。
  3. 无法处理 JOIN:多表连接需要类型层的关系代数,超出模板字面量类型能力。
  4. 递归深度限制:长 SQL 可能触发 “Type instantiation is excessively deep”。
  5. 不支持表达式列:SELECT COUNT(*) 无法直接推导类型。

在这些场景下,应回退到运行时校验(如 zod)或手动类型注解。


11. 事件系统与回调类型

11.1 类型安全的事件名派生

type EventMap = {
  click: { x: number; y: number };
  hover: { target: HTMLElement };
  focus: { element: HTMLElement };
  submit: { form: HTMLFormElement };
};

type EventName = keyof EventMap;
type EventHandler<E extends EventName> = (payload: EventMap[E]) => void;

class TypedEventEmitter {
  private handlers: { [E in EventName]?: EventHandler<E>[] } = {};

  on<E extends EventName>(event: E, handler: EventHandler<E>): void {
    (this.handlers[event] ??= []).push(handler);
  }

  emit<E extends EventName>(event: E, payload: EventMap[E]): void {
    this.handlers[event]?.forEach(h => h(payload));
  }

  off<E extends EventName>(event: E, handler: EventHandler<E>): void {
    this.handlers[event] = this.handlers[event]?.filter(h => h !== handler);
  }
}

const emitter = new TypedEventEmitter();

// 类型安全的事件注册
emitter.on('click', ({ x, y }) => console.log(x, y));
emitter.emit('click', { x: 10, y: 20 });

// emitter.on('click', ({ wrong }) => {});  // Error: wrong 不在 payload 中
// emitter.emit('click', { x: 10 });         // Error: 缺少 y

11.2 事件名变换

// 从动作名派生事件名
type Action = 'click' | 'hover' | 'submit' | 'change';
type EventName<T extends string> = `on${Capitalize<T>}`;
type HandlerMap<T extends string> = {
  [K in T as EventName<K>]: (e: Event) => void;
};

type Handlers = HandlerMap<Action>;
// {
//   onClick: (e: Event) => void;
//   onHover: (e: Event) => void;
//   onSubmit: (e: Event) => void;
//   onChange: (e: Event) => void;
// }

11.3 Vue 风格的 emits 类型

type EmitsMap = {
  'update:modelValue': (value: string) => void;
  'change': (value: string, oldValue: string) => void;
  'submit': (payload: { id: number; data: unknown }) => void;
};

type EmitsHandlers<T> = {
  [K in keyof T]: T[K];
};

function defineEmits<T extends Record<string, (...args: any[]) => any>>(
  emits: T
): {
  <K extends keyof T>(event: K, ...args: Parameters<T[K]>): void;
} {
  return ((event: string, ...args: any[]) => {
    console.log(event, args);
  }) as any;
}

const emit = defineEmits<EmitsHandlers<EmitsMap>>({});

// 类型安全调用
emit('update:modelValue', 'new value');
emit('submit', { id: 1, data: { foo: 'bar' } });

// emit('update:modelValue');  // Error: 缺少参数
// emit('unknown', 'x');        // Error: 未知事件

12. 对比分析

12.1 与其他语言的字符串类型

语言类型层字符串操作模式匹配递归性能
TypeScript模板字面量类型infer + 模板支持(尾递归优化)中等
Haskell类型族 + GHC.TypeLitsSymbol 模式匹配支持编译慢
Scala 3MatchType + String 单例String 单例 + inline支持编译慢
Rustconst 泛型 + &str不支持不支持快
Flow不支持不支持不支持-
OCaml不支持不支持不支持-

12.2 与运行时模板字符串对比

维度模板字面量类型运行时模板字符串
求值时机编译期运行时
输入类型字面量类型任意值
输出类型字面量类型或 stringstring
联合处理笛卡尔积取实际值
Unicode简化 ASCII完整 Unicode
locale不支持支持
性能影响编译时间影响运行时间
可读性复杂时难读简单

12.3 与正则表达式对比

维度模板字面量类型正则表达式
求值时机编译期运行时
量词通过递归实现* + ?
字符类不支持[a-z]
捕获组infer()
反向引用不支持\1
性能编译期运行时

12.4 与 zod/io-ts 对比

维度模板字面量类型zod/io-ts
求值时机编译期运行时
输入来源字面量任意值
类型信息类型层类型层 + 运行时
性能编译期影响运行时影响
动态数据不适用适用
复杂度高中

12.5 与 Haskell 类型族对比

-- Haskell 的类型级字符串操作
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

import GHC.TypeLits

type family Append (a :: Symbol) (b :: Symbol) :: Symbol where
  Append "" b = b
  Append a b = AppendSymbol a b

-- 使用
type T = Append "hello" "world"

TypeScript 模板字面量类型在表达能力上接近 Haskell 的 Symbol 类型族,但语法更直观,且不需要 UndecidableInstances 编译选项。


13. 常见陷阱与修复

13.1 陷阱:贪婪匹配的误解

错误代码:

type FirstWord<S extends string> =
  S extends `${infer W} ${string}` ? W : S;

type F = FirstWord<'hello world foo'>; // 期望 'hello',实际 'hello world'

原因:TypeScript 的 infer 默认非贪婪,但当后面跟着固定模式时,会调整为贪婪以满足整体匹配。这里 ${string} 匹配任意字符串,导致 W 尽可能长。

修复:使用更具体的模式:

type FirstWord<S extends string> =
  S extends `${infer W} ${infer Rest}`
    ? W extends `${string} `  // 检查 W 是否含空格
      ? never
      : W
    : S;

// 或者更优雅的方案:递归提取
type FirstWordRec<S extends string, Acc extends string = ''> =
  S extends `${infer C}${infer Rest}`
    ? C extends ' '
      ? Acc
      : FirstWordRec<Rest, `${Acc}${C}`>
    : Acc;

type F2 = FirstWordRec<'hello world foo'>; // 'hello'

13.2 陷阱:递归深度超限

错误代码:

type Reverse<S extends string> =
  S extends `${infer C}${infer Rest}` ? `${Reverse<Rest>}${C}` : S;

type R = Reverse<'a'.repeat(100)>; // Error: Type instantiation is excessively deep

原因:递归深度超过约 50 层,且非尾递归。

修复:使用尾递归优化:

type ReverseTail<S extends string, Acc extends string = ''> =
  S extends `${infer C}${infer Rest}` ? ReverseTail<Rest, `${C}${Acc}`> : Acc;

type R2 = ReverseTail<'a'.repeat(100)>; // OK

13.3 陷阱:联合类型不展开

错误代码:

type Wrap<T> = `[${T}]`;

type W = Wrap<'a' | 'b' | 'c'>; // 期望 '[a]' | '[b]' | '[c]',实际 '[a | b | c]'

原因:模板字面量类型本身会分发,但仅当占位符在模板内时。Wrap 的 T 没有在模板内使用,所以不分发。

修复:将 T 放入模板字面量:

type Wrap<T extends string> = `[${T}]`;

type W = Wrap<'a' | 'b' | 'c'>; // '[a]' | '[b]' | '[c]'

注意:实际上 TypeScript 4.1+ 对模板字面量类型的联合分发是自动的,无需特殊处理。上述示例在 4.7+ 中已正确分发。

13.4 陷阱:键重映射的 never 过滤

错误代码:

type StringValues<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K] extends string ? T[K] : never;
};

interface Mixed {
  a: string;
  b: number;
  c: boolean;
}

type S = StringValues<Mixed>;
// 期望 { a: string },实际 { a: string; b: never; c: never }

原因:键重映射的 never 会过滤键,但值类型仍需显式处理。

修复:

type StringValues<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  a: string;
  b: number;
  c: boolean;
}

type S = StringValues<Mixed>; // { a: string }

键重映射为 never 时,该键被完全移除,无需在值类型中处理。

13.5 陷阱:模板字面量类型与 string 的归一化

错误代码:

type Prefix<T extends string> = `prefix-${T}`;

const x: Prefix<string> = 'prefix-anything'; // OK,但类型为 `prefix-${string}`
const y: Prefix<string> = 'other';            // Error

function f<T extends string>(s: T): Prefix<T> {
  return `prefix-${s}` as Prefix<T>;
}

const z = f('hello'); // Prefix<'hello'> = 'prefix-hello'
const w = f(someStringVariable); // Prefix<string> = `prefix-${string}`

原因:当 T 为 string 时,Prefix<string> 保持为模板字面量类型 `prefix-${string}`,而非归一化为 string。这是有意为之,允许更精确的类型约束。

修复:根据需求选择:

// 方案 1:保持模板字面量类型
type Prefix<T extends string> = `prefix-${T}`;

// 方案 2:当 T 为 string 时归一化
type PrefixOrString<T extends string> = string extends T ? string : `prefix-${T}`;

13.6 陷阱:Unicode 字符处理

错误代码:

type T = Uppercase<'istanbul'>; // 'ISTANBUL'

// 期望 'İSTANBUL'(土耳其语 İ)
const turkishUpper = 'istanbul'.toLocaleUpperCase('tr-TR'); // 'İSTANBUL'

// 但类型层无法实现
type T2 = Uppercase<'istanbul'>; // 'ISTANBUL',与 locale 无关

原因:TypeScript 的 Uppercase<S> 在编译期使用简化的 ASCII 大小写映射,不支持 locale-sensitive 转换。

修复:在需要 locale-sensitive 转换的场景,回退到运行时:

function toUpperLocale<S extends string>(s: S, locale?: string): string {
  return s.toLocaleUpperCase(locale);
}

const t = toUpperLocale('istanbul', 'tr-TR'); // 'İSTANBUL',但返回 string

13.7 陷阱:编译性能

错误代码:

// 复杂的递归模板字面量类型,应用于大联合类型
type DeepTransform<T> = T extends string
  ? `transformed-${Uppercase<T>}`
  : T extends object
    ? { [K in keyof T as `prefix-${K & string}`]: DeepTransform<T[K]> }
    : T;

// 应用于大类型
type BigType = { a: string; b: { c: string; d: { e: string } } };
type Transformed = DeepTransform<BigType>; // 编译慢

原因:递归模板字面量类型在大类型上展开慢,TypeScript 5.7 之前尤为明显。

修复:

  1. 拆分类型:将复杂类型拆分为多个小类型。
  2. 使用 satisfies:在运行时验证类型。
  3. 限制递归深度:通过 infer 模式限制递归层数。
  4. 缓存类型:将常用类型定义为 type 别名,避免重复求值。
// 拆分方案
type TransformKey<K extends string> = `prefix-${K}`;
type TransformValue<T> = T extends string
  ? `transformed-${Uppercase<T>}`
  : T extends object
    ? DeepTransform<T>
    : T;

type DeepTransform<T> = {
  [K in keyof T as TransformKey<K & string>]: TransformValue<T[K]>;
};

14. 工程实践

14.1 何时使用模板字面量类型

适用场景:

  1. 静态字符串约束:路由、事件名、CSS 类名等运行时已知的字符串。
  2. 类型派生:从已有类型派生新类型,如 getter/setter。
  3. API 客户端:根据 API 路径推导请求/响应类型。
  4. 配置验证:根据配置 schema 派生配置类型。

不适用场景:

  1. 运行时动态字符串:用户输入、数据库查询结果等。
  2. 复杂解析:完整 SQL、JSON、XML 等,应使用专门的解析器。
  3. 性能敏感:大型代码库中,过度使用会拖慢编译。

14.2 性能优化建议

  1. 使用尾递归:TypeScript 4.5+ 支持尾递归优化。
  2. 避免深度嵌套:限制递归深度在 30 层以内。
  3. 缓存类型:用 type 别名避免重复求值。
  4. 拆分大类型:将复杂类型拆分为多个小类型。
  5. 使用 satisfies:在运行时验证类型,避免复杂的类型推导。

14.3 类型测试

使用 @tsd 或 expect-type 进行类型测试:

import { expectTypeOf } from 'expect-type';
import { Getters, Params, Replace } from './types';

// 测试 Getters
type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
expectTypeOf<PersonGetters>().toEqualTypeOf<{
  getName: () => string;
  getAge: () => number;
}>();

// 测试 Params
expectTypeOf<Params<'/users/:id'>>().toEqualTypeOf<{ id: string }>();
expectTypeOf<Params<'/users'>>().toEqualTypeOf<{}>();

// 测试 Replace
expectTypeOf<Replace<'hello world', 'world', 'TS'>>().toEqualTypeOf<'hello TS'>();

14.4 文档与可读性

复杂类型应配备注释与示例:

/**
 * ExtractRouteParams - 从路由字符串提取参数类型
 *
 * @example
 * type P = ExtractRouteParams<'/users/:id'>; // { id: string }
 * type P2 = ExtractRouteParams<'/posts/:postId/comments/:commentId'>;
 * // { postId: string; commentId: string }
 */
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer P}/${infer Rest}`
    ? { [K in P | keyof ExtractRouteParams<Rest>]: string }
    : T extends `${string}:${infer P}`
      ? { [K in P]: string }
      : {};

14.5 与 IDE 集成

模板字面量类型在 VSCode 中的 hover 提示较为冗长,可通过 @ts-expect-error 或 satisfies 优化:

// 原始(hover 时显示完整展开)
type Result = Params<'/posts/:postId/comments/:commentId'>;

// 优化(hover 时显示别名)
type PostCommentRouteParams = Params<'/posts/:postId/comments/:commentId'>;
type Result = PostCommentRouteParams;

15. 案例研究

15.1 案例一:tRPC 风格的端到端类型安全 API

// 路由定义
type AppRouter = {
  'user.get': { input: { id: number }; output: { id: number; name: string } };
  'user.create': { input: { name: string; age: number }; output: { id: number } };
  'user.update': { input: { id: number; name?: string }; output: { success: boolean } };
  'post.list': { input: { page: number; limit: number }; output: { posts: Array<{ id: number; title: string }> } };
};

// 客户端调用类型
type ProcedureName = keyof AppRouter;
type ProcedureInput<N extends ProcedureName> = AppRouter[N]['input'];
type ProcedureOutput<N extends ProcedureName> = AppRouter[N]['output'];

class TypedClient {
  async call<N extends ProcedureName>(
    name: N,
    input: ProcedureInput<N>
  ): Promise<ProcedureOutput<N>> {
    // 实现省略
    return {} as ProcedureOutput<N>;
  }
}

const client = new TypedClient();

// 类型安全调用
const user = await client.call('user.get', { id: 1 });
// user: { id: number; name: string }

const newUser = await client.call('user.create', { name: 'Alice', age: 30 });
// newUser: { id: number }

// client.call('user.get', { name: 'Alice' });  // Error: 缺少 id
// client.call('unknown.proc', {});              // Error: 未知过程

15.2 案例二:Tailwind CSS 类型系统

// Tailwind 配置类型
type TailwindConfig = {
  spacing: {
    0: '0px';
    1: '0.25rem';
    2: '0.5rem';
    4: '1rem';
    8: '2rem';
  };
  colors: {
    primary: { 50: '#eff6ff'; 500: '#3b82f6'; 900: '#1e3a8a' };
    gray: { 100: '#f3f4f6'; 500: '#6b7280'; 900: '#111827' };
  };
  fontSize: {
    sm: '0.875rem';
    base: '1rem';
    lg: '1.125rem';
    xl: '1.25rem';
  };
};

// 生成工具类名
type SpacingClass = `m-${keyof TailwindConfig['spacing']}` | `p-${keyof TailwindConfig['spacing']}`;
// 'm-0' | 'm-1' | 'm-2' | 'm-4' | 'm-8' | 'p-0' | ...

type ColorClass =
  | `text-${keyof TailwindConfig['colors']}-${keyof TailwindConfig['colors'][keyof TailwindConfig['colors']]}`
  | `bg-${keyof TailwindConfig['colors']}-${keyof TailwindConfig['colors'][keyof TailwindConfig['colors']][number]}`;
// 'text-primary-50' | 'text-primary-500' | ... | 'bg-gray-100' | ...

type FontSizeClass = `text-${keyof TailwindConfig['fontSize']}`;
// 'text-sm' | 'text-base' | 'text-lg' | 'text-xl'

type ClassName = SpacingClass | ColorClass | FontSizeClass;

function cls(...classes: ClassName[]): string {
  return classes.join(' ');
}

// 类型安全调用
const c = cls('m-4', 'text-primary-500', 'text-lg');
// const invalid = cls('m-3');  // Error: 3 不在 spacing 中
// const invalid2 = cls('text-unknown-500');  // Error: unknown 不在 colors 中

15.3 案例三:i18n 类型安全

type EnMessages = {
  'welcome': 'Welcome, {name}!';
  'goodbye': 'Goodbye, {name}!';
  'cart.items': 'You have {count} items in your cart.';
  'user.profile': '{name} ({age} years old)';
};

type ZhMessages = {
  'welcome': '欢迎,{name}!';
  'goodbye': '再见,{name}!';
  'cart.items': '购物车中有 {count} 件商品。';
  'user.profile': '{name}({age} 岁)';
};

// 提取占位符
type ExtractPlaceholders<S extends string> =
  S extends `${string}{${infer P}}${infer Rest}`
    ? P | ExtractPlaceholders<Rest>
    : never;

type MessageParams<K extends keyof EnMessages> = {
  [P in ExtractPlaceholders<EnMessages[K]>]: string | number;
};

function t<K extends keyof EnMessages>(
  key: K,
  params: MessageParams<K>
): string {
  // 实现省略
  return '';
}

// 类型安全调用
t('welcome', { name: 'Alice' });
t('cart.items', { count: 5 });
t('user.profile', { name: 'Bob', age: 30 });

// t('welcome', {});           // Error: 缺少 name
// t('unknown.key', {});       // Error: 未知 key
// t('welcome', { wrong: 'x' }); // Error: 错误参数

15.4 案例四:Vue props 类型派生

import { defineComponent, PropType } from 'vue';

type PropDefinitions = {
  title: { type: StringConstructor; required: true };
  count: { type: NumberConstructor; default: 0 };
  items: { type: PropType<string[]>; default: () => [] };
  callback: { type: PropType<(x: number) => void>; required: true };
};

type PropsFromDefinitions<T> = {
  [K in keyof T]: T[K] extends { required: true }
    ? T[K]['type'] extends StringConstructor
      ? string
      : T[K]['type'] extends NumberConstructor
        ? number
        : T[K]['type'] extends PropType<infer U>
          ? U
          : never
    : T[K] extends { default: infer D }
      ? T[K]['type'] extends StringConstructor
        ? string | undefined
        : T[K]['type'] extends NumberConstructor
          ? number | undefined
          : T[K]['type'] extends PropType<infer U>
            ? U | undefined
            : never
      : never;
};

type Props = PropsFromDefinitions<PropDefinitions>;
// {
//   title: string;
//   count: number | undefined;
//   items: string[] | undefined;
//   callback: (x: number) => void;
// }

15.5 案例五:JSON Schema 类型派生

type JsonSchema = {
  type: 'object';
  properties: {
    name: { type: 'string' };
    age: { type: 'number' };
    email: { type: 'string'; optional: true };
  };
};

type SchemaType<S> =
  S extends { type: 'object'; properties: infer P }
    ? { [K in keyof P as P[K] extends { optional: true } ? never : K]: SchemaType<P[K]> } &
      { [K in keyof P as P[K] extends { optional: true } ? K : never]?: SchemaType<P[K]> }
    : S extends { type: 'string' }
      ? string
      : S extends { type: 'number' }
        ? number
        : S extends { type: 'boolean' }
          ? boolean
          : never;

type T = SchemaType<JsonSchema>;
// {
//   name: string;
//   age: number;
//   email?: string;
// }

15.6 案例六:HTTP 状态码类型

type HttpStatusCode = 
  | 100 | 101 | 102 | 103
  | 200 | 201 | 202 | 203 | 204 | 205 | 206
  | 300 | 301 | 302 | 303 | 304 | 307 | 308
  | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 409 | 410 | 418 | 422 | 429
  | 500 | 501 | 502 | 503 | 504;

type HttpStatusName =
  HttpStatusCode extends 100 | 101 | 102 | 103 ? 'Informational'
  : HttpStatusCode extends 200 | 201 | 202 | 203 | 204 | 205 | 206 ? 'Success'
  : HttpStatusCode extends 300 | 301 | 302 | 303 | 304 | 307 | 308 ? 'Redirection'
  : HttpStatusCode extends 400 | 401 | 402 | 403 | 404 | 405 | 406 | 409 | 410 | 418 | 422 | 429 ? 'ClientError'
  : HttpStatusCode extends 500 | 501 | 502 | 503 | 504 ? 'ServerError'
  : 'Unknown';

type ApiResponse<T, Code extends HttpStatusCode = 200> = {
  status: Code;
  statusText: HttpStatusName;
  data: Code extends 200 | 201 ? T : never;
  error: Code extends 400 | 500 ? { message: string; code: string } : undefined;
};

function createResponse<T, C extends HttpStatusCode>(
  code: C,
  data: T
): ApiResponse<T, C> {
  return {
    status: code,
    statusText: '' as HttpStatusName,
    data: data as any,
    error: undefined as any,
  };
}

const r1 = createResponse(200, { name: 'Alice' });
// { status: 200; statusText: 'Success'; data: { name: 'Alice' }; error: undefined }

const r2 = createResponse(404, { name: 'Alice' });
// { status: 404; statusText: 'ClientError'; data: never; error: undefined }

填空题知识点讲解

  1. TypeScript 4.1 引入的模板字面量类型允许在类型层使用 ES2015 模板字符串语法,其占位符内只能放置____类型或____类型。

    • 答案:string;string 的联合
    • Bloom:remember
  2. 内置类型 Uppercase<S> 的实现原理是调用 ECMAScript 抽象操作____,该操作在 ES2024 中被规范化为 String.prototype.toUpperCase 的底层语义。

    • 答案:StringToUpperCase
    • Bloom:understand
  3. 在模式匹配 ${infer Head}${infer Tail} 中,TypeScript 采用____策略,Head 仅匹配首个字符,这与正则表达式的____模式一致。

    • 答案:非贪婪;懒惰
    • Bloom:analyze
  4. 模板字面量类型与联合类型组合时会产生____,即每个联合分支独立代入模板,最终结果为所有组合的联合。

    • 答案:笛卡尔积
    • Bloom:understand
  5. TypeScript 4.5 引入的____优化使得尾位置的递归模板字面量类型不再触发深度错误,识别条件是递归调用必须出现在条件类型的____。

    • 答案:尾递归;直接分支位置
    • Bloom:understand
  6. 在键重映射 [K in keyof T as NewKey<K>]: T[K] 中,当 NewKey<K> 返回____时,该键被过滤掉,不会出现在结果类型中。

    • 答案:never
    • Bloom:apply

16.3 代码修复题

  1. 以下代码试图通过模板字面量类型提取路由参数名,但只能解析单个参数,无法解析 /users/:userId/posts/:postId。请修复。
type Params<T extends string> = T extends `${string}:${infer P}`
  ? { [K in P]: string }
  : {};
  • 答案:
    type Params<T extends string> =
      T extends `${string}:${infer P}/${infer Rest}`
        ? { [K in P | keyof Params<Rest>]: string }
        : T extends `${string}:${infer P}`
          ? { [K in P]: string }
          : {};
  • Bloom:apply
  1. 以下代码试图实现 Join<A, Sep> 将联合类型 A 用 Sep 连接为字符串字面量,但报错 “Type instantiation is excessively deep”。请优化为尾递归版本。
type Join<A extends string, Sep extends string> =
  A extends `${infer First}${Sep}${infer Rest}`
    ? `${First}${Sep}${Join<Rest, Sep>}`
    : A;
  • 答案:
    type Join<A extends string, Sep extends string, Acc extends string = ''> =
      A extends `${infer First}${Sep}${infer Rest}`
        ? Join<Rest, Sep, Acc extends '' ? First : `${Acc}${Sep}${First}`>
        : Acc extends '' ? A : `${Acc}${Sep}${A}`;
  • Bloom:create
  1. 以下代码试图派生对象的所有方法名为 'on${MethodName}' 形式,但报错 “Type ‘keyof T’ is not assignable to type ‘string’“。请修复。
type OnMethods<T> = {
  [K in keyof T as `on${Capitalize<K>}`]: T[K];
};
  • 答案:
    type OnMethods<T> = {
      [K in keyof T as `on${Capitalize<K & string>}`]: T[K];
    };
  • Bloom:apply
  1. 以下代码试图实现字符串反转,但 TypeScript 报 “Type instantiation is excessively deep”。请改写为尾递归版本。
type Reverse<S extends string> =
  S extends `${infer C}${infer Rest}` ? `${Reverse<Rest>}${C}` : S;
  • 答案:
    type Reverse<S extends string, Acc extends string = ''> =
      S extends `${infer C}${infer Rest}` ? Reverse<Rest, `${C}${Acc}`> : Acc;
  • Bloom:apply

16.4 开放题

  1. 你正在为一个 ORM 设计类型安全的查询构造器,需要根据 SQL 字符串字面量推导查询结果的列类型。请描述:

    (1) 如何使用模板字面量类型解析 SELECT id, name, age FROM users WHERE id = $1 中的列名列表; (2) 如何将列名列表映射到对应的类型(假设有 type Schema = { users: { id: number; name: string; age: number } }); (3) 该方案的局限性是什么,何时应回退到运行时校验。

    请结合 TypeScript 4.5+ 的尾递归优化讨论性能。

    • 答案:详见第 10 节”SQL 类型安全”。
    • Bloom:create
  2. 比较 TypeScript 模板字面量类型与 Haskell 的 Symbol 类型族在以下维度的差异:表达能力、编译性能、可读性、生态成熟度。在什么场景下你会选择 TypeScript 而非 Haskell?反之呢?

    • 答案要点:
      • 表达能力:Haskell 更强,支持 UndecidableInstances 与完整的类型族;TypeScript 受深度限制。
      • 编译性能:TypeScript 通常更快,但复杂递归会拖慢;Haskell GHC 类型族编译慢。
      • 可读性:TypeScript 模板字面量语法更直观;Haskell 需要理解类型族与 Symbol。
      • 生态成熟度:Haskell 类型级编程生态更成熟(singletons、first-class-families);TypeScript 在前端生态占主导。
      • 选择 TypeScript:前端、Node.js、与现有 JS 生态集成的场景。
      • 选择 Haskell:需要严格证明、依赖类型、或与 Formal Methods 工具链集成的场景。
    • Bloom:evaluate
  3. 设计一个类型安全的 i18n 系统,要求:

    • 消息键为字符串字面量联合类型
    • 消息中的占位符(如 {name}、{count})必须作为参数对象传入
    • 多语言切换时,所有语言的占位符必须一致(否则编译错误)

    请给出完整的类型定义与使用示例。

    • 答案:详见第 15.3 节”i18n 类型安全”。
    • Bloom:create
  4. 解释为什么 TypeScript 模板字面量类型的 infer 采用非贪婪匹配策略,而非贪婪匹配。这一设计在什么场景下会带来困惑?应如何规避?

    • 答案要点:
      • 原因:非贪婪匹配更直观地对应”分割”语义(如 Split<'a-b-c', '-'> 期望 ['a', 'b', 'c'] 而非 ['a-b', 'c'])。
      • 困惑场景:当用户期望”匹配最长前缀”时(如 FirstWord<'hello world foo'> 期望 'hello',但实际可能匹配 'hello world',取决于模式)。
      • 规避方法:使用更具体的模式,或显式递归提取字符。
    • Bloom:evaluate

18.1 官方文档

18.2 经典论文

  • Hosoya, H., Vouillon, J., and Pierce, B. C. 2005. Regular expression types for XML. ACM TOPLAS.
  • Xi, H. and Pfenning, F. 1999. Dependent types in practical programming. POPL ‘99.
  • Tabareau, N. et al. 2018. Equations for the working Coq user. CPP 2018.

18.3 开源项目

18.5 相关 FANDEX 文档

  • TypeScript 递归类型与深度操作 — 深入递归条件类型与尾递归优化
  • TypeScript 映射类型与键重映射 — 与模板字面量类型结合的键变换
  • TypeScript 条件类型与映射类型 — 模板字面量类型的基础
  • TypeScript 类型体操实用模式 — 综合应用
  • TypeScript 协变与逆变 — 模板字面量类型的型变性质

附录 A:内置字符串操作类型速查

类型签名示例等价运行时
Uppercase<S>(S extends string ? uppercase : never)Uppercase<'hello'> → 'HELLO's.toUpperCase()
Lowercase<S>(S extends string ? lowercase : never)Lowercase<'HELLO'> → 'hello's.toLowerCase()
Capitalize<S>(S extends string ? capitalize : never)Capitalize<'hello'> → 'Hello's[0].toUpperCase() + s.slice(1)
Uncapitalize<S>(S extends string ? uncapitalize : never)Uncapitalize<'Hello'> → 'hello's[0].toLowerCase() + s.slice(1)

注意事项:

  • 四个操作类型对联合类型自动分发。
  • 当输入为 string 时,输出为 string(保留模板字面量信息时除外)。
  • 当输入为 any 时,输出为 string。
  • 当输入为 never 时,输出为 never。
  • 当输入包含非字符串类型时,TypeScript 报错。

附录 B:模式匹配速查表

模式语义示例
`${infer P}`提取整个字符串'hello' → P = 'hello'
`${infer Head}${infer Tail}`提取首字符与剩余'hello' → Head = 'h', Tail = 'ello'
`${infer P}${infer Rest}`等价于上面同上
`prefix${infer Rest}`提取前缀后的部分'prefixHello' → Rest = 'Hello'
`${infer P}suffix`提取后缀前的部分'HelloSuffix' → P = 'Hello'
`${infer A}-${infer B}`按分隔符分割(首次)'a-b-c' → A = 'a', B = 'b-c'
`${infer A}:${infer B}/${infer C}`多重分割'x:y/z' → A = 'x', B = 'y', C = 'z'
`${string}${infer P}`提取最后一字符'hello' → P = 'o'
`${infer P}${string}`提取第一字符'hello' → P = 'h'

贪婪性:

  • TypeScript 默认非贪婪,但具体行为取决于模式。
  • ${infer Head}${infer Tail} 中 Head 仅匹配 1 字符(最短匹配)。
  • ${infer A}-${infer B} 中 A 匹配到第一个 - 前(非贪婪)。

附录 C:性能基准

C.1 编译时间基准

以下是在中等规模项目(10 万行代码)中的编译时间影响:

类型复杂度编译时间(增量)编译时间(全量)
无模板字面量类型2.1s18s
简单模板字面量类型(10 处)2.3s19s
中等复杂度(含递归,50 处)2.8s24s
高复杂度(深度递归,100 处)4.5s42s
极高复杂度(无尾递归优化)8.2s95s

C.2 优化建议

  1. 使用尾递归:将非尾递归改为尾递归,编译时间可减少 50% 以上。
  2. 限制递归深度:通过 extends 约束限制递归层数。
  3. 拆分类型:将复杂类型拆分为多个小类型,分别缓存。
  4. 避免大联合:大联合类型的笛卡尔积展开慢,应拆分或使用 satisfies。
  5. 使用 @tsperf:测量类型性能,定位瓶颈。

附录 D:决策流程图

D.1 是否使用模板字面量类型

flowchart TD
    T0["开始"]
    T1["输入是否为字面量字符串?"]
    T2["否 → 不使用,回退到运行时校验"]
    T3["是"]
    T4["是否需要类型层推导?"]
    T5["否 → 直接使用字面量类型"]
    T6["是"]
    T7["递归深度是否 < 30 层?"]
    T8["否 → 考虑运行时校验或拆分类型"]
    T9["是"]
    T10["是否可改写为尾递归?"]
    T11["是 → 使用尾递归优化"]
    T12["否 → 评估性能影响"]
    T13["使用模板字面量类型"]
    T0 --> T1
    T0 --> T2
    T0 --> T3
    T3 --> T4
    T0 --> T5
    T0 --> T6
    T6 --> T7
    T0 --> T8
    T0 --> T9
    T9 --> T10
    T0 --> T11
    T0 --> T12
    T12 --> T13

D.2 选择模式匹配策略

flowchart TD
    T0["需要分割字符串"]
    T1["分割符固定?"]
    T2["是 → 使用 `${infer A}${Sep}${infer B}`"]
    T3["否 → 递归字符提取"]
    T4["需要全部分割?"]
    T5["是 → 递归调用 Split"]
    T6["否 → 单次匹配"]
    T7["需要尾递归优化?"]
    T8["是 → 引入 Acc 累加器"]
    T9["否 → 直接递归"]
    T0 --> T1
    T0 --> T2
    T0 --> T3
    T3 --> T4
    T0 --> T5
    T0 --> T6
    T6 --> T7
    T7 --> T8
    T7 --> T9

E.1 基础概念

  • 能说出模板字面量类型的语法
  • 能说出四个内置字符串操作类型
  • 能解释模板字面量类型与运行时模板字符串的差异
  • 能说出 TypeScript 4.1 引入该特性的年份

E.2 模式匹配

  • 能用 infer 提取字符串前缀
  • 能用 infer 提取字符串后缀
  • 能实现字符串分割(Split)
  • 能实现字符串替换(Replace / ReplaceAll)
  • 能解释非贪婪匹配的语义

E.3 递归

  • 能实现递归模板字面量类型(如 Reverse)
  • 能识别尾递归位置
  • 能将非尾递归改写为尾递归
  • 能解释深度限制的原因

E.4 工程应用

  • 能用模板字面量类型派生 getter/setter
  • 能实现路由参数类型推导
  • 能实现 CSS 属性类型
  • 能实现事件系统类型
  • 能实现 SQL 列类型推导(简单场景)

E.5 性能

  • 能识别编译性能瓶颈
  • 能使用尾递归优化
  • 能拆分复杂类型
  • 能使用 satisfies 替代部分类型推导

E.6 高级

  • 能解释模板字面量类型与正则表达式类型的关系
  • 能比较 TypeScript 与 Haskell 类型级字符串操作
  • 能设计类型安全的 i18n 系统
  • 能设计类型安全的 ORM 查询构造器
  • 能评估何时回退到运行时校验

本文最后审阅日期:2026-07-20。审阅团队:FANDEX Content Engineering Team。如发现错误或建议改进,请提交 issue 至 FANDEX 仓库。

基本模板字面量类型

基本写法:基本模板字面量类型 type <类型> = \<前缀>${<类型>}“

// 基本模板字面量类型
type Greeting = `hello ${string}`

基本写法:使用模板字面量类型 let <变量>: <类型> = "<值>"

// 使用模板字面量类型
let greeting: Greeting = "hello world"

联合类型模板字面量

换行写法:联合类型模板字面量 type <类型> = \<前缀>${<类型1> | <类型2>}“

// 联合类型模板字面量
type Side = "left" | "right"
type Direction = `turn ${Side}`

基本写法:使用联合类型模板字面量 let <变量>: <类型> = "<值>"

// 使用联合类型模板字面量
let direction: Direction = "turn left"

多变量模板字面量

换行写法:多变量模板字面量 type <类型> = \${<类型1>}_${<类型2>}“

// 多变量模板字面量
type Border = `${"top" | "bottom"}-${"left" | "right"}`

基本写法:使用多变量模板字面量 let <变量>: <类型> = "<值>"

// 使用多变量模板字面量
let corner: Border = "top-left"

字符串操作类型

基本写法:使用 Uppercase 转大写 type <类型> = Uppercase<<字符串类型>>

// 将字符串类型转为大写
type Upper = Uppercase<"hello">  // "HELLO"

基本写法:使用 Lowercase 转小写 type <类型> = Lowercase<<字符串类型>>

// 将字符串类型转为小写
type Lower = Lowercase<"HELLO">  // "hello"

基本写法:使用 Capitalize 首字母大写 type <类型> = Capitalize<<字符串类型>>

// 将字符串类型首字母大写
type Capitalized = Capitalize<"hello">  // "Hello"

基本写法:使用 Uncapitalize 首字母小写 type <类型> = Uncapitalize<<字符串类型>>

// 将字符串类型首字母小写
type Uncapitalized = Uncapitalize<"Hello">  // "hello"

模板字面量与映射类型

换行写法:使用模板字面量重映射键 type <类型><<T>> = { [P in keyof T as \get_${P & string}`]: T[P] }`

// 使用模板字面量为键添加前缀
type Getters<T> = {
    [P in keyof T as `get_${P & string}`]: () => T[P]
}

换行写法:使用 Capitalize 重映射键 type <类型><<T>> = { [P in keyof T as \on${Capitalize<P & string>}`]: T[P] }`

// 使用 Capitalize 为键添加 on 前缀
type EventHandlers<T> = {
    [P in keyof T as `on${Capitalize<P & string>}`]: (value: T[P]) => void
}

模板字面量与 infer

换行写法:使用 infer 推断模板字面量 type <类型> = <S> extends \prefix_${infer }` ? : never`

// 使用 infer 推断模板字面量中的类型
type RemovePrefix<S> = S extends `prefix_${infer T}` ? T : never

换行写法:推断字符串前缀 type <类型> = <S> extends \${infer }_suffix` ? : never`

// 推断字符串前缀
type GetPrefix<S> = S extends `${infer Prefix}_suffix` ? Prefix : never

换行写法:推断字符串两部分 type <类型> = <S> extends \{infer <First>}_\{infer }` ? [, ] : never`

// 推断字符串的两部分
type Split<S> = S extends `${infer First}_${infer Second}` ? [First, Second] : never

模板字面量实战

换行写法:生成 getter 方法名 type <类型><<T>> = { [P in keyof T as \get${Capitalize<P & string>}`]: () => T[P] }`

// 为所有属性生成 getter 方法名
type Getters<T> = {
    [P in keyof T as `get${Capitalize<P & string>}`]: () => T[P]
}

换行写法:生成 setter 方法名 type <类型><<T>> = { [P in keyof T as \set${Capitalize<P & string>}`]: (<值>: T[P]) => void }`

// 为所有属性生成 setter 方法名
type Setters<T> = {
    [P in keyof T as `set${Capitalize<P & string>}`]: (value: T[P]) => void
}

换行写法:生成事件处理器 type <类型><<T>> = { [P in keyof T as \on${Capitalize<P & string>}`]: (<值>: T[P]) => void }`

// 为所有属性生成事件处理器
type EventHandlers<T> = {
    [P in keyof T as `on${Capitalize<P & string>}`]: (value: T[P]) => void
}

模板字面量与联合类型

换行写法:从联合类型生成字符串 type <类型> = \${<联合类型1>}_${<联合类型2>}“

// 从联合类型生成所有组合
type Prefix = "get" | "set"
type Suffix = "Name" | "Age"
type MethodName = `${Prefix}${Suffix}`

基本写法:使用联合类型模板字面量 let <变量>: <类型> = "<值>"

// 使用联合类型模板字面量
let method: MethodName = "getName"

模板字面量与条件类型

换行写法:条件类型与模板字面量 type <类型> = <S> extends \${infer }` ? : never`

// 条件类型与模板字面量组合
type ExtractString<S> = S extends `${infer T}` ? T : never

换行写法:检查字符串前缀 type <类型> = <S> extends \prefix_${string}` ? true : false`

// 检查字符串是否有指定前缀
type HasPrefix<S> = S extends `prefix_${string}` ? true : false

模板字面量与 keyof

换行写法:从对象键生成事件名 type <类型> = \on${Capitalize<keyof <接口> & string>}“

// 从对象键生成事件名
interface User {
    name: string
    age: number
}

type UserEvent = `on${Capitalize<keyof User & string>}`

基本写法:使用 keyof 模板字面量 let <变量>: <类型> = "<值>"

// 使用 keyof 模板字面量
let event: UserEvent = "onName"

模板字面量与递归

换行写法:递归处理字符串 type <类型> = <S> extends \{infer <First>}\{infer }` ? <处理> : `

// 递归处理字符串
type Reverse<S> = S extends `${infer First}${infer Rest}` ? `${Reverse<Rest>}${First}` : S

换行写法:递归替换字符 type <类型> = <S> extends \{infer <Before>}_\{infer }` ? <类型><`{<Before>}-\{}`> : `

// 递归替换下划线为连字符
type Replace<S> = S extends `${infer Before}_${infer After}` ? Replace<`${Before}-${After}`> : S

模板字面量与类型推断

换行写法:推断函数名 type <类型> = <F> extends \${infer }${string}` ? : never`

// 推断函数名前缀
type GetPrefix<F> = F extends `${infer Prefix}${string}` ? Prefix : never

换行写法:推断属性路径 type <类型> = <P> extends \${infer }.${infer }` ? <处理> :

`

// 推断属性路径
type GetFirstPath<P> = P extends `${infer First}.${infer Rest}` ? First : P

模板字面量与对象

换行写法:从对象生成配置类型 interface <接口> { <属性>: <类型> } type <类型> = \—${<接口>[“<属性>”]}“

// 从对象生成配置类型
interface Config {
    host: string
    port: number
}

type ConfigKey = `--${keyof Config}`

基本写法:使用对象模板字面量 let <变量>: <类型> = "<值>"

// 使用对象模板字面量
let key: ConfigKey = "--host"

模板字面量与枚举

换行写法:从枚举生成字符串 enum <枚举> { <成员1>, <成员2> } type <类型> = \${<枚举>}“

// 从枚举生成字符串类型
enum Status {
    Active = "ACTIVE",
    Inactive = "INACTIVE",
}

type StatusString = `${Status}`

基本写法:使用枚举模板字面量 let <变量>: <类型> = "<值>"

// 使用枚举模板字面量
let status: StatusString = "ACTIVE"

模板字面量与工具类型

换行写法:实现 Join 工具类型 type <类型> = <T extends string[], <分隔符> extends string> = <T> extends [infer <First>, ...infer <Rest>] ? <Rest> extends [] ? <First> : \${}${<分隔符>}${<类型><, <分隔符>>}` : never`

// 实现 Join 工具类型
type Join<T extends string[], D extends string> =
    T extends [infer First, ...infer Rest]
    ? Rest extends [] ? First : `${First & string}${D}${Join<Rest, D>}`
    : never

换行写法:实现 Split 工具类型 type <类型> = <S extends string, <分隔符> extends string> = <S> extends \{infer <First>}\{<分隔符>}${infer }` ? [, …<类型><, <分隔符>>] : []`

// 实现 Split 工具类型
type Split<S extends string, D extends string> =
    S extends `${infer First}${D}${infer Rest}`
    ? [First, ...Split<Rest, D>]
    : [S]

模板字面量与路径

换行写法:生成属性路径 type <类型><<T>> = <T> extends object ? { [P in keyof T & string]: \${P}.${<类型><T[P]>>}` }[keyof T] : never`

// 生成嵌套属性路径
type Path<T> = T extends object
    ? { [P in keyof T & string]: `${P}.${Path<T[P]>}` }[keyof T]
    : never

换行写法:生成简单属性路径 type <类型><<T>> = <T> extends object ? { [P in keyof T & string]: P }[keyof T] : never

// 生成简单属性路径
type SimplePath<T> = T extends object
    ? { [P in keyof T & string]: P }[keyof T]
    : never

模板字面量与 CSS

换行写法:生成 CSS 属性名 type <类型> = \${<属性>}-${<值>}“

// 生成 CSS 属性名
type Property = "margin" | "padding"
type Side = "top" | "bottom" | "left" | "right"
type CSSProperty = `${Property}-${Side}`

基本写法:使用 CSS 模板字面量 let <变量>: <类型> = "<值>"

// 使用 CSS 模板字面量
let css: CSSProperty = "margin-top"

模板字面量与 API

换行写法:生成 API 路径 type <类型> = \/api/${<路径>}“

// 生成 API 路径
type Endpoint = "users" | "posts" | "comments"
type APIPath = `/api/${Endpoint}`

基本写法:使用 API 模板字面量 let <变量>: <类型> = "<值>"

// 使用 API 模板字面量
let path: APIPath = "/api/users"