news 2026/9/30 8:13:26

TypeScript 类型体操实战:用模板字面量类型实现 `Trim<T>` 去除两端空白

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript 类型体操实战:用模板字面量类型实现 `Trim<T>` 去除两端空白
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

导读

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, 4803

related: 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 '>拆解为若干步:

  1. ' \n\t foo bar '匹配${' '}${' \n\t foo bar '},递归;
  2. 依次剥除第二个、第三个空格,以及'\n'、'\t';
  3. 剩余' foo bar '的首字符是' ',继续剥除;
  4. 剩余'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 '>:

  1. 末字符' '是空白,拆出' foo bar \n\t',递归;
  2. 依次剥除'\n'、'\t'、末尾空格、空格;
  3. 剩余' 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.ts

test-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

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:Gutenberg 的 `@wordpress/deprecated` 包:从 API 用法到 CHANGELOG 变更史的技术全解
下一篇:FastF1 v2.1.1 实时时序数据记录与回放:Live Timing Data 完整实战指南

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 8:12:14

大数据预处理全攻略:从数据清洗到工程化实践

1. 为什么数据预处理是大数据项目的隐形地基1.1 数据预处理到底是什么我入行大数据这么多年&#xff0c;有个感受越来越深&#xff1a;真正决定一个项目成败的&#xff0c;往往不是算法模型有多高级&#xff0c;也不是集群规模有多大&#xff0c;而是最不起眼的那一步——数据预…

作者头像 李华
网站建设 2026/9/30 8:12:14

EOS8.3.3附件删除权限控制:多上传人场景下只能删自己上传的

附件删除权限这件事&#xff0c;在EOS8.3.3上做过的朋友应该都懂那种纠结&#xff1a;需求一句话&#xff0c;“附件允许多选&#xff0c;不是同一个人上传的&#xff0c;只能删除自己上传的”&#xff0c;写起来却要拆出一整套逻辑。多附件、多上传人、前端的删除入口、后端的…

作者头像 李华
网站建设 2026/9/30 8:10:35

赫斯曼交换机命令行手册:从Console登录到VLAN与环网配置实操

简介&#xff1a;这份赫斯曼交换机命令行简易用户手册面向网络运维与工程实施人员&#xff0c;聚焦工业交换机在项目交付中的基础配置与配置文件上传场景&#xff0c;适合具备一定网络基础、需要快速上手命令行操作的读者。资源包共1个docx文档&#xff0c;约125KB&#xff0c;…

作者头像 李华
网站建设 2026/9/30 8:10:32

ESP-IDF组件开发核心原理与VS Code实践指南

1. 为什么在 VS Code 里“创建组件”不是点个按钮就完事&#xff1f;很多人第一次用 ESP-IDF 在 VS Code 里开发&#xff0c;看到官方文档里写着“创建新组件”&#xff0c;下意识就去菜单栏翻“File → New Component”——结果什么都没找到。我当年也是这样&#xff0c;在终端…

作者头像 李华
网站建设 2026/9/30 8:09:43

4G LTE基础完全指南:蜂窝网络、核心网元与关键参数调试

蜂窝无线网络这个词&#xff0c;做通信的几乎天天挂在嘴边&#xff0c;但真要让谁用大白话把4G LTE这件事讲清楚&#xff0c;很多人反而卡壳。我最早接触LTE是好几年前做网优测试的时候&#xff0c;揣着测试手机到处跑&#xff0c;看RSRP、盯SINR、打点、拉网&#xff0c;那时候…

作者头像 李华