- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
导读
Trim<T>是 type-challenges 仓库中一道中等难度(medium)、标签为template-literal的类型挑战,要求实现一个接收精确字符串字面量类型并返回去除两端空白后新字符串类型的工具类型。本文将以 questions/00108-medium-trim/README.md 为骨架,结合仓库内的 template.ts 起点、test-cases.ts 测试用例,以及同系列的 TrimLeft / TrimRight 姊妹题,完整讲解递归模板字面量类型的推导原理、实现步骤与边界情况处理,帮助读者掌握字符串类型层面的"裁剪"能力。
挑战速览:题目要求
题目背景
本题由 Anthony Fu(@antfu)提出,收录于 questions/00108-medium-trim/ 目录下,其元信息在 info.yml 中标明:
difficulty: medium title: Trim tags: template-literal author: github: antfu name: Anthony Fu related: 106, 4803related: 106, 4803指向两道姊妹题:
- 106・Trim Left:仅去除左侧空白;
- 4803・Trim Right:仅去除右侧空白。
它们共同构成了字符串空白处理的三件套,建议按106 → 108 → 4803的顺序循序渐进。
核心需求
实现Trim<T>,它接受一个明确的字符串类型(exact string type,即字符串字面量类型而非string宽类型),并返回一个新字符串类型,其中两端的空白符都已被删除。
题目给出的期望示例:
type trimmed = Trim<' Hello World '> // expected to be 'Hello World'验收标准:测试用例
仓库在每个题目目录下都提供了由@type-challenges/utils的Equal与Expect封装的类型级断言测试,位于 questions/00108-medium-trim/test-cases.ts:
import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Trim<'str'>, 'str'>>, // 无空白 Expect<Equal<Trim<' str'>, 'str'>>, // 单个前导空格 Expect<Equal<Trim<' str'>, 'str'>>, // 多个前导空格 Expect<Equal<Trim<'str '>, 'str'>>, // 多个尾随空格 Expect<Equal<Trim<' str '>, 'str'>>, // 两端均有空格 Expect<Equal<Trim<' \n\t foo bar \t'>, 'foo bar'>>, // 制表符与换行符 Expect<Equal<Trim<''>, ''>>, // 空字符串 Expect<Equal<Trim<' \n\t '>, ''>>, // 纯空白字符串 ]从用例可以提炼出必须满足的语义边界:
| 场景 | 输入 | 期望输出 | 说明 |
|---|---|---|---|
| 无空白 | 'str' | 'str' | 原样返回 |
| 前导空白 | ' str' | 'str' | 多个空格递归剥除 |
| 尾随空白 | 'str ' | 'str' | 多个空格递归剥除 |
| 混合空白 | ' \n\t foo bar \t' | 'foo bar' | 需覆盖空格、\n、\t三种字符 |
| 空字符串 | '' | '' | 递归终止,不能报错 |
| 纯空白字符串 | ' \n\t ' | '' | 空白被完全剥除后落空 |
这里的Equal来自仓库的 utils/index.d.ts,是一个基于函数类型推断实现的结构相等判断工具;Expect<T extends true> = T则用于在类型层面强制断言成立。
空白字符集合:Whitespace联合类型
类型级trim与运行时String.prototype.trim()一样,需要明确"哪些字符算空白"。题目测试用例覆盖了三种字符:空格' '、换行'\n'、制表符'\t'(见' \n\t foo bar \t'用例)。
因此第一步是定义空白字符的联合类型:
type Whitespace = ' ' | '\n' | '\t'这是一个关键的建模决策:把"空白"建模为有限字符联合,而不是单个字符。这样做的直接好处是,在与模板字面量进行模式匹配时,infer可以自由匹配联合中的任一成员,从而一次覆盖所有空白字符。
说明:本题的官方测试集只要求覆盖
' '、'\n'、'\t'三种。若你的实现希望更通用,可扩展联合,例如'\r'(回车)乃至'\u3000'(全角空格),但必须在满足本仓库测试用例的前提下进行,不要为了"更全面"而引入题外复杂度。
逐层剥离:模板字面量的条件递归
核心模式:${infer Head}${Tail}拆解
模板字面量类型(Template Literal Type)在infer配合下可以解构字符串的首个字符:
type FirstChar<S extends string> = S extends `${infer Head}${string}` ? Head : never同样的机制用于递归剥除左侧空白:只要字符串的首字符是空白字符,就将其丢弃,并继续对剩余部分递归处理:
type TrimLeft<S extends string> = S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S推导过程可以用TrimLeft<' \n\t foo bar '>拆解为若干步:
' \n\t foo bar '匹配${' '}${' \n\t foo bar '},递归;- 依次剥除第二个、第三个空格,以及
'\n'、'\t'; - 剩余
' foo bar '的首字符是' ',继续剥除; - 剩余
'foo bar '首字符'f'不是空白,走: S分支,返回'foo bar '。
该实现的正确性可以由姊妹题 questions/00106-medium-trimleft/test-cases.ts 中的用例验证,其中包括TrimLeft<' str '> → 'str '与TrimLeft<' \n\t foo bar '> → 'foo bar '。
对称处理右侧:尾递归到头部
右侧空白无法像左侧那样直接拆"首字符",但可以把问题翻转:不去看字符串末尾,而是不断拆掉"非末尾部分",让右侧空白逐步暴露到字符串首部。
具体做法是让字符串匹配${infer Rest}${Whitespace}(末字符为空白),然后将前面部分Rest递归传入,直到末尾不再是空白:
type TrimRight<S extends string> = S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S推导TrimRight<' foo bar \n\t '>:
- 末字符
' '是空白,拆出' foo bar \n\t',递归; - 依次剥除
'\n'、'\t'、末尾空格、空格; - 剩余
' foo bar'末字符'r'非空白,返回。
对应用例见 questions/04803-medium-trim-right/test-cases.ts,其中TrimRight<' str '> → ' str'验证了右侧剥离、左侧保留的语义。
组合成 Trim:两次递归的串联
Trim<T>的本质是先执行 TrimLeft、再执行 TrimRight。最直白的写法是组合两个已实现的工具类型:
type Trim<S extends string> = TrimLeft<TrimRight<S>>或先左后右、结果一致:
type Trim<S extends string> = TrimRight<TrimLeft<S>>这里需要思考一个问题:为什么两次递归叠加不会破坏已剥净的一侧?关键在于TrimLeft只检查首字符、TrimRight只检查末字符,二者互不干扰——左侧剥完后首字符必然非空白,TrimRight不会误伤它;反之亦然。
关于实现风格的补充说明
- 若不想复用姊妹类型,也可以写成一个单一递归的版本:交替剥除两端空白,直到首尾均非空白才终止。这类实现可行,但可读性通常不如"左+右"组合清晰。
- 模板字面量类型在 TS 4.1 起可用;递归类型实例化深度受编译器限制,对这类逐字符剥除的短字符串场景完全足够。本仓库 package.json 中 devDependencies 声明
typescript: ^5.3.3,在该版本下上述写法均能正常通过测试。 - 条件类型在裸类型参数上会触发分发(distributive),而
S extends string后TrimLeft<TrimRight<S>>中的S是明确的字面量,不涉及分发问题;但为严谨起见,工具类型对外仍建议保留S extends string约束。
类型级 trim 的意义与适用边界
适用场景
运行时String.prototype.trim()处理的是值,而Trim<T>处理的是类型。当字符串字面量类型进入类型系统(例如被as const断言、或被用作字面量联合成员)后,Trim<T>能在编译期完成空白归一化,从而:
- 在泛型约束中统一用户传入的字符串格式(如表单键、路由参数、枚举值字符串);
- 配合其他模板字面量工具类型做字符串解析、路径拼接等场景的前置清洗;
- 在声明文件中描述"去除空白后的输出类型",让 IDE 提示直接呈现结果类型。
边界与限制
Trim<T>只处理类型层面的字符串,不会改变任何运行时行为,也不是对trim()的类型声明替代。- 输入的
T必须是字符串字面量类型(或字面量联合)。若传入宽类型string,条件类型无法拆解出精确的infer,会退回: S分支原样返回。 - 空白字符集合由
Whitespace决定,需要覆盖哪些字符完全取决于题目要求,可自行扩展。
如何验证你的实现
本地类型检查
仓库采用 pnpm workspace 管理,@type-challenges/utils作为 workspace 包提供。在仓库根目录完成依赖安装后,可直接对单个题目文件做类型检查:
pnpm install npx tsc --noEmit questions/00108-medium-trim/test-cases.tstest-cases.ts中每个Expect<Equal<...>>都是类型级断言:只有当Trim<' \n\t foo bar \t'>被推导为'foo bar'时,Equal才返回true,Expect才满足T extends true,编译期断言通过;反之类型检查将报错。因此,测试用例通过即等价于类型推导正确。
对照相关题目
- 先做 106・TrimLeft,验证单向剥离;
- 再做 108・Trim(本文题目),组合出双向剥离;
- 最后做 4803・TrimRight,闭环三件套。
三者的测试用例(分别位于 00106-medium-trimleft/test-cases.ts、00108-medium-trim/test-cases.ts、04803-medium-trim-right/test-cases.ts)在空白字符覆盖上保持一致(空格、\n、\t),可以作为交叉验证。
参考答案汇总
将上述思路汇总为可直接通过全部用例的完整实现:
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> = TrimRight<TrimLeft<S>>验证关键用例:
type a = Trim<' Hello World '> // 'Hello World' type b = Trim<' \n\t foo bar \t'> // 'foo bar' type c = Trim<''> // '' type d = Trim<' \n\t '> // ''a:两侧空格先被TrimLeft剥除,再被TrimRight剥除,得到'Hello World';b:TrimLeft剥除' \n\t '后为'foo bar \t',TrimRight再剥除'\t',得到'foo bar';c、d:空字符串与纯空白字符串在递归剥离中自然走到终止分支,得到''。
小结
Trim<T>是模板字面量类型 + 条件递归的经典组合:先用Whitespace联合类型建模空白字符集合,再分别用${Whitespace}${infer Rest}与${infer Rest}${Whitespace}两个方向的模式匹配实现左、右单向剥离,最后串联为双向Trim。通过 test-cases.ts 的 8 个类型断言(覆盖无空白、单侧、双侧、混合空白、空串、纯空白六类场景),可以完整验证实现的正确性。掌握了这一模式,你就具备了用类型系统处理字符串裁剪、格式归一化等场景的核心能力,也为后续挑战中更复杂的字符串解析类型做好了铺垫。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
Type Challenges 中等题解:用模板字面量类型实现 `Trim<T>` 去除字符串两端空白
Type Challenges 中等题解:用模板字面量类型实现 Trim<T 去除字符串两端空白 Trim<T 是 Type Challenges(type c
示例工程type-challenges 第 108 题 Trim 解析:用模板字面量类型实现两端空白去除
type challenges 第 108 题 Trim 解析:用模板字面量类型实现两端空白去除 导读 Trim 是 type challenges 仓库中编号
示例工程TypeScript 类型挑战 106:用模板字面量类型实现 TrimLeft 去除字符串左侧空白
TypeScript 类型挑战 106:用模板字面量类型实现 TrimLeft 去除字符串左侧空白 本篇文章围绕 type challenges 仓库中的第 1
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考