- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
本篇文章围绕 type-challenges 仓库中「去除两端空白字符(Trim)」这道中等难度题目展开,完整还原其解题思路、递归模式匹配原理,并结合仓库内的测试用例逐条验证。读完本文,你将掌握如何在 TypeScript 类型层面利用模板字面量类型与infer实现字符串裁剪,并能独立迁移这套技巧去解决 TrimLeft、TrimRight 等一系列相邻题目。
题目速览:我们要实现什么
题目位于仓库 questions/00108-medium-trim 目录,题目难度标记为「中級 / medium」,所属主题标签为#template-literal(模板字面量类型),由 Anthony Fu 提出。
需求一句话概括:实现Trim<T>,它接收一个精确的字符串类型,返回一个删除了两端空白字符的新字符串类型。
原题给出的示例:
type trimmed = Trim<' Hello World '> // expected to be 'Hello World'也就是说,这并非运行时对值的裁剪,而是纯粹的类型级(compile-time)字符串处理:输入是字面量字符串类型,输出也是经过裁剪后的字面量字符串类型。
打开该题目的 template.ts,可以看到需要补全的占位实现:
type Trim<S extends string> = anyS extends string约束了入参必须可赋值给字符串类型,我们要做的就是把any替换成真正的递归类型逻辑。
前置知识:模板字面量类型与 infer 模式匹配
要解这道题,先掌握两个 TypeScript 的核心能力:
- 模板字面量类型(Template Literal Types):与 JS 的模板字符串语法一致,但它作用于类型空间。例如
type Greet = `Hello ${string}`表示"以 Hello 开头、后接任意字符串"的类型。 - 条件类型 + infer 模式匹配:
T extends \${infer Rest}` ? ... : ...可以在类型层面把字符串"拆开",infer` 负责捕获其中的一部分。
两者结合,就能写出类型版的字符串"取子串"逻辑。仓库 guides 目录下专门收录了相关指南,例如 infer 指南 讲解infer在条件类型中的捕获用法,递归指南 讲解如何在类型层面进行递归推导,本题正是这两种技巧的组合应用。
第一步:定义空白字符联合类型
观察测试用例会发现,需要裁剪的空白字符不止空格' ',还包括换行符'\n'和制表符'\t'(详见下文测试用例解读)。因此先定义一个联合类型:
type Whitespace = ' ' | '\n' | '\t'这样在后续的模式匹配中,${Whitespace}就能一次性匹配三种空白字符中的任意一种。
第二步:TrimLeft——递归吃掉左侧空白
核心思路是:只要字符串以空白字符开头,就把这个字符"剪掉",然后对剩余部分继续递归。当不再以空白开头时,返回剩余的字符串。
type TrimLeft<S extends string> = S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S逐行解读:
S extends \${Whitespace}${infer Rest}`:尝试把S匹配为"一个空白字符 + 剩余部分Rest`";- 若匹配成功(字符串确实以空白开头),递归调用
TrimLeft<Rest>继续裁剪; - 若匹配失败(开头不是空白),直接返回
S,即当前已经"左侧干净"的字符串。
以TrimLeft<' abc'>为例,递归过程为:' abc'→' abc'→' abc'→'abc'(开头为a,不再匹配,返回)。
这一思路与仓库中 TrimLeft 题目 完全一致,00106 的模板 同样以any占位等待实现。
第三步:TrimRight——递归吃掉右侧空白
对称地,裁剪右侧空白就是把匹配模式反过来:只要字符串以空白字符结尾,就剪掉最后一个字符,继续递归。
type TrimRight<S extends string> = S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S注意这里infer Rest在模板中的位置不同:${infer Rest}${Whitespace}表示"任意前缀 + 一个空白字符结尾"。递归终止条件是结尾不再是空白字符。
以TrimRight<'abc '>为例:'abc '→'abc '→'abc '→'abc'(结尾为c,返回)。仓库中 TrimRight 题目(编号 4803,作者 Yugang Cao)即考察这一对称实现。
第四步:合体——Trim 组合两侧裁剪
Trim<T>就是"先裁剪右侧、再裁剪左侧"的组合(顺序无影响,也可以反过来):
type Trim<S extends string> = TrimLeft<TrimRight<S>>完整可运行的解法如下:
type Whitespace = ' ' | '\n' | '\t' type TrimLeft<S extends string> = S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S type TrimRight<S extends string> = S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S type Trim<S extends string> = TrimLeft<TrimRight<S>>也可以把两侧裁剪合并进一个类型,直接对左右同时递归:
type Trim<S extends string> = S extends `${Whitespace}${infer Rest}` ? Trim<Rest> : S extends `${infer Rest}${Whitespace}` ? Trim<Rest> : S两种写法都能通过题目测试,前者更贴近"先拆分后组合"的模块化思路,后者递归次数更少、单类型即可完成。
逐条验证:对照仓库测试用例
该题目的 test-cases.ts 给出了 8 组断言,是检验解法正确性的唯一标准。我们逐条模拟:
| 输入 | 期望输出 | TrimLeft 之后 | TrimRight 之后(Trim = TrimLeft∘TrimRight 先右后左) |
|---|---|---|---|
'str' | 'str' | 'str' | 'str' |
' str' | 'str' | 'str' | ' str'→ 左裁 →'str' |
' str' | 'str' | 'str' | 右裁无变化,左裁去 5 个空格 →'str' |
'str ' | 'str' | 'str ' | 右裁去 3 个空格 →'str' |
' str ' | 'str' | 右裁 →' str',左裁 →'str' | 'str' |
' \n\t foo bar \t' | 'foo bar' | 右裁去'\t',左裁去' \n\t ' | 'foo bar' |
'' | '' | '' | '' |
' \n\t ' | '' | 右裁全吃掉 →'' | '' |
注意第 6 条Trim<' \n\t foo bar \t'>和第 8 条Trim<' \n\t '>:它们专门验证对换行符\n与制表符\t的处理,这正是Whitespace联合类型必须包含'\n'与'\t'的原因。如果只定义type Whitespace = ' ',这两条用例将无法通过。
空字符串''也是必须覆盖的边界:'' extends \${Whitespace}${infer Rest}`不成立,直接返回''`,符合预期。
验证机制:断言如何工作
测试用例中出现了两个辅助类型Expect与Equal,它们来自仓库的 utils/index.d.ts:
export type Expect<T extends true> = T export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : falseEqual<X, Y>通过构造两个函数类型并比较其条件类型结果,能够严格判断两个类型是否完全相等(比简单的X extends Y更严谨,可识别联合类型分配等细微差异);Expect<T extends true>要求传入的必须恰好是true,一旦Equal判定不相等,Expect就会产生类型错误,从而让类型测试"编译失败即测试失败"。
因此,只要把解法填入 template.ts,再与 test-cases.ts 一同编译,8 组断言全部通过即代表实现正确。
递进关系:从 TrimLeft 到 Trim 的题型家族
这道题在仓库中并非孤立存在,而是与相邻题目构成一条清晰的递进链路:
- 00106 · TrimLeft:只裁剪左侧空白,其测试用例如
TrimLeft<' str '>期望'str ',即右侧空白保留——见 00106 的 test-cases.ts; - 00108 · Trim(本题):左右同时裁剪;
- 04803 · TrimRight:只裁剪右侧空白,其测试如
TrimRight<' str '>期望' str'——见 04803 的 test-cases.ts。
可以发现,仓库对这组题目的定位是"先用单侧裁剪打基础,再组合出双侧裁剪"。TrimLeft 题目的说明 与 TrimRight 题目的说明 中也都互相引用了彼此,方便读者按递进顺序练习。
延伸思考与边界条件
为什么不用内置工具类型?TypeScript 标准库并没有内置字符串裁剪的工具类型,这正是此类"类型体操"题目的意义所在:通过递归与模板字面量类型,在类型空间复刻
String.prototype.trim的能力,用于从字面量类型中提取干净内容(如解析路由参数、用户输入标签等场景)。空白字符集合可扩展:本题只需要空格、换行、制表符三类。若业务中需要处理回车
'\r'、换页'\f'等字符,只需向Whitespace联合类型中追加成员即可,递归逻辑无需任何改动。递归深度限制:类型级递归受 TypeScript 的递归深度上限约束。对本题常见的短字符串(几到几十个字符)完全够用;若裁剪超长字符串,可考虑先裁左再裁右以减少单方向递归次数。
infer的位置决定裁剪方向:${Whitespace}${infer Rest}裁剪头部,${infer Rest}${Whitespace}裁剪尾部——理解这一对称性是吃透整个题型家族的关键,也是 infer 指南 强调的核心直觉。
小结
通过 type-challenges 第 108 题 Trim,我们完整实践了"模板字面量类型 + 条件类型 + infer 模式匹配 + 递归"这一类型体操黄金组合:先以Whitespace联合类型圈定空白字符范围,再用递归分别实现左侧、右侧裁剪,最后组合出Trim<T>,并通过仓库自带的 8 组测试断言完成验证。这套方法不仅适用于本题,也直接延伸至 TrimLeft、TrimRight 以及更广泛的字符串解析类类型设计。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
Flue 渠道 Webhook 校验机制:如何防止伪造事件攻击
Flue 渠道 Webhook 校验机制:如何防止伪造事件攻击 Flue 是一个面向 AI 智能体的沙箱框架(sandbox agent framework),
人工智能大模型AI AgentAgent 框架工具调用Agent 沙箱MCP Clients告别字符串首尾空格:Type Challenges中的Trim类型完全解析
告别字符串首尾空格:Type Challenges中的Trim类型完全解析 你是否曾在处理用户输入时被意外的空格困扰?在TypeScript中,如何在编译阶段就
示例工程告别字符串处理烦恼:Type-Challenges DropChar类型实战指南
告别字符串处理烦恼:Type Challenges DropChar类型实战指南 你是否还在为TypeScript中字符串处理的复杂类型定义而头疼?是否想提升泛
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考