- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
type-challenges 是一套带在线评测系统的 TypeScript 类型体操题库(项目说明)。本文聚焦其中第 2070 号中等难度挑战Drop Char:从字符串类型中剔除指定字符,通过条件类型、模板字面量类型(template literal types)与infer三个核心工具的组合,实现一个纯粹的"类型层面"的字符串处理函数。读完本文,你将掌握模板字面量类型的递归匹配套路,理解infer在模式匹配中的角色,并能够独立解出同类的字符串类型操作题。
挑战概览:题目要求与已知条件
根据 questions/02070-medium-drop-char/README.md,题目定义如下:
- 难度:中等(medium)
- 标签:
template-literal、infer - 作者:CaptainOfPhB
- 目标:定义一个类型
DropChar<S, C>,把字符串S中出现的所有字符C剔除,返回处理后的字符串类型。
题面给出的示例:
type Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'也就是说,S与C都是字符串字面量类型,我们要剔除S中所有等于C的字符,保留其余字符并保持原有顺序。题目的元数据记录在 questions/02070-medium-drop-char/info.yml 中:difficulty: medium、tags: template-literal, infer,并注明related: 2059(即 Hard 难度的 Drop String 挑战)。
起点模板与测试用例:先看清"验收标准"
仓库为每个挑战都提供了起始模板(待填空的占位类型)与测试用例。本题的 template.ts 非常简单:
type DropChar<S, C> = any我们的任务就是把any替换为真正可用的类型逻辑,同时保证全部测试通过。测试用例位于 test-cases.ts:
import type { Equal, Expect } from '@type-challenges/utils' type cases = [ // @ts-expect-error Expect<Equal<DropChar<'butter fly!', ''>, 'butterfly!'>>, Expect<Equal<DropChar<'butter fly!', ' '>, 'butterfly!'>>, Expect<Equal<DropChar<'butter fly!', '!'>, 'butter fly'>>, Expect<Equal<DropChar<' butter fly! ', ' '>, 'butterfly!'>>, Expect<Equal<DropChar<' b u t t e r f l y ! ', ' '>, 'butterfly!'>>, Expect<Equal<DropChar<' b u t t e r f l y ! ', 'b'>, ' u t t e r f l y ! '>>, Expect<Equal<DropChar<' b u t t e r f l y ! ', 't'>, ' b u e r f l y ! '>>, ]逐条解读这些用例,可以提炼出本题的验收要求:
用例输入S | 剔除字符C | 期望结果 | 考察点 |
|---|---|---|---|
'butter fly!' | ''(空字符串) | 类型错误(@ts-expect-error) | 空字符串作为剔除字符应被拒绝 |
'butter fly!' | ' ' | 'butterfly!' | 基础空格剔除 |
'butter fly!' | '!' | 'butter fly' | 剔除普通字符 |
' butter fly! ' | ' ' | 'butterfly!' | 首尾连续空格全部剔除 |
' b u t t e r f l y ! ' | ' ' | 'butterfly!' | 所有目标字符出现处都剔除 |
' b u t t e r f l y ! ' | 'b' | ' u t t e r f l y ! ' | 只剔指定字符,其他字符与空格保留 |
' b u t t e r f l y ! ' | 't' | ' b u e r f l y ! ' | 多次出现全部剔除 |
其中用到的Equal与Expect工具类型定义在 utils/index.d.ts:Expect<T extends true> = T,Equal<X, Y>通过函数的参数逆变比较((<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2))严格判断两个类型是否相等。这意味着解法必须产生与期望完全相同的字符串字面量类型,任何多出的空格、丢失的字符都会导致测试失败。
核心解法:条件类型 + infer + 模板字面量递归
题目标签template-literal与infer已经给出了解题方向。TypeScript 4.1 引入的模板字面量类型允许在类型层面用${}拼接字符串,并且可以与infer配合做"模式匹配 + 提取子串":
type DropChar<S extends string, C extends string> = S extends `${infer Head}${infer Tail}` ? Head extends C ? DropChar<Tail, C> : `${Head}${DropChar<Tail, C>}` : S逐层分析这个解法的执行流程:
- 递归终止条件:当
S extends${infer Head}${infer Tail}`` 不成立时,说明S已退化到空字符串(或其他无法再拆分的字面量),此时直接返回S本身。 - 拆解字符串:
${infer Head}${infer Tail}会把字符串拆成"首字符 + 剩余部分"。这里Head被推断为单个字符,Tail为剩余子串。 - 判断是否命中:用条件类型
Head extends C判断当前字符是否就是要剔除的字符。注意 TypeScript 的条件类型在联合类型上是分布式的(distributive conditional types),但由于本题中C固定为一个字面量、Head每次匹配到单一字符,这里表现为一次普通的判断。 - 剔除或保留:命中则直接丢弃
Head,只递归处理Tail;未命中则把Head保留,拼接到递归结果的前面。 - 逐字符累积:递归不断推进
Tail,最终完整遍历整个字符串,所有命中字符被移除,其余字符按原顺序重组。
边界情况:为什么空字符串要被拒绝
第一个测试用例特意标注了@ts-expect-error:
// @ts-expect-error Expect<Equal<DropChar<'butter fly!', ''>, 'butterfly!'>>@ts-expect-error要求下一行代码必然产生编译错误。也就是说,设计上DropChar的第二参数C不允许传入空字符串。为此,可以对类型参数加上约束:
type DropChar<S extends string, C extends string> = ...这样C必须是string类型;而''(空字符串字面量)本身就是string的子类型,直接写C extends string仍会放行空串。更严谨的做法是显式约束C必须是非空字符串:
type DropChar<S extends string, C extends string & { length: 1 }> = ...利用{ length: 1 }把C限定为恰好一个字符长度的字面量。这样DropChar<'butter fly!', ''>就会因不满足约束而报错,正好满足@ts-expect-error的期望;同时' '、'b'、'!'这些单字符串都能通过约束。这也是对"从字符串中剔除一个指定字符"这一题意的类型级强化。
从源码结构看:为什么这里用模板字面量 + infer
在类型层面处理字符串,TypeScript 提供了两条路径:
- 字符串字面量联合 /
keyof工具:适合对象键名、枚举等有限集合,不适合任意内容字符串; - 模板字面量类型 +
infer:可以像正则一样对字符串做"匹配并提取"的递归拆解,是本题这类"任意字符串处理"题目的标准解法。
模板字面量类型在推断时的关键行为是:${infer A}${infer B}对未知字符串做拆解时,TypeScript 会优先让第一个infer捕获尽可能短的内容(单字符),剩余部分交给第二个infer,因此Head/Tail的递归拆解是确定且可终止的。这也解释了为什么解法不需要额外的"长度判断"——递归天然会在字符串耗尽时落入终止分支。
仓库的 guides/infer.md 与 guides/key-in.md 是官方为infer与key in主题准备的专项指南,本题的标签与之一致,可作为深入学习infer推导规则的配套资料。
延伸进阶:从 Drop Char 到 Hard 的 Drop String
info.yml与 README 页脚都标注了关联挑战2059・Drop String(README.md,难度 hard)。两者的关系是一脉相承的:Drop Char 只剔除一个字符,而 Drop String 要剔除一组字符:
type Butterfly = DropString<'foobar!', 'fb'> // 'ooar!'Drop String 的测试用例(test-cases.ts)展示了更强的场景:第二个参数本身是一个字符串,其中每个字符都要被剔除。从源码结构看,Drop String 的经典解法正是在 Drop Char 的思路上加一层外层递归:先把R拆成字符,对每个字符应用"逐个剔除",再递归处理下一个字符。也就是说,学会 Drop Char 等于拿到了 Drop String 的一半解法,这正是官方把两者设为关联题目的原因。
如何验证你的解法
type-challenges 的每个题目目录都自带test-cases.ts与template.ts,验证方式与普通 TS 项目一致:
- 在 template.ts 中填入你的类型实现;
- 用
tsc(仓库 devDependencies 中锁定 TypeScript^5.3.3,见 package.json)对 test-cases.ts 做类型检查,没有报错即代表全部Equal断言通过; - 也可在 tsconfig.json 与 tsconfig.base.json 配置的严格模式下运行,保证解法的类型安全性。
补充一点:本文给出的解法以"每步拆单字符"实现,若想追求更紧凑的写法,也可以利用模板字面量一次匹配更长片段(如S extends${infer A}${C}${infer B}`` 直接按目标字符定位),再对两侧递归,核心原理仍是"模板字面量匹配 + infer 提取 + 条件类型分发 + 递归推进",掌握本文的拆解思路后即可灵活变形。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
type-challenges 中等题精解:用模板字面量类型实现 `Capitalize<T>`
type challenges 中等题精解:用模板字面量类型实现 Capitalize<T 本篇文章基于开源仓库 type challenges(Collect
示例工程type-challenges 中等题 612:用模板字面量类型实现 KebabCase 字符串转换
type challenges 中等题 612:用模板字面量类型实现 KebabCase 字符串转换 本文围绕 type challenges 仓库第 612
示例工程type-challenges 中等题 298:用模板字面量类型与递归实现 Length of String
type challenges 中等题 298:用模板字面量类型与递归实现 Length of String 本文围绕 type challenges 题库中编
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考