☰
类型体操实战:用模板字面量类型实现 Trim——从 type-challenges 第 108 题看字符串的类型级裁剪
2026/9/30 2:31:43 网站建设 项目流程
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

本篇文章围绕 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> = any

S extends string约束了入参必须可赋值给字符串类型,我们要做的就是把any替换成真正的递归类型逻辑。

前置知识:模板字面量类型与 infer 模式匹配

要解这道题,先掌握两个 TypeScript 的核心能力:

  1. 模板字面量类型(Template Literal Types):与 JS 的模板字符串语法一致,但它作用于类型空间。例如type Greet = `Hello ${string}`表示"以 Hello 开头、后接任意字符串"的类型。
  2. 条件类型 + 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 : false
  • Equal<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 题目的说明 中也都互相引用了彼此,方便读者按递进顺序练习。

延伸思考与边界条件

  1. 为什么不用内置工具类型?TypeScript 标准库并没有内置字符串裁剪的工具类型,这正是此类"类型体操"题目的意义所在:通过递归与模板字面量类型,在类型空间复刻String.prototype.trim的能力,用于从字面量类型中提取干净内容(如解析路由参数、用户输入标签等场景)。

  2. 空白字符集合可扩展:本题只需要空格、换行、制表符三类。若业务中需要处理回车'\r'、换页'\f'等字符,只需向Whitespace联合类型中追加成员即可,递归逻辑无需任何改动。

  3. 递归深度限制:类型级递归受 TypeScript 的递归深度上限约束。对本题常见的短字符串(几到几十个字符)完全够用;若裁剪超长字符串,可考虑先裁左再裁右以减少单方向递归次数。

  4. 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

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

相关推荐

上一篇:res-downloader 免抓包下载器教程:如何快速下载视频号视频与网页资源
下一篇:Repomix FAQ 与故障排查完全指南:私有仓库、远程打包、Token 削减与 MCP 集成

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询