☰
type-challenges 中等挑战 2070:用模板字面量类型与 infer 实现 Drop Char
2026/10/2 2:10:25 网站建设 项目流程
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

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

逐层分析这个解法的执行流程:

  1. 递归终止条件:当S extends${infer Head}${infer Tail}`` 不成立时,说明S已退化到空字符串(或其他无法再拆分的字面量),此时直接返回S本身。
  2. 拆解字符串:${infer Head}${infer Tail}会把字符串拆成"首字符 + 剩余部分"。这里Head被推断为单个字符,Tail为剩余子串。
  3. 判断是否命中:用条件类型Head extends C判断当前字符是否就是要剔除的字符。注意 TypeScript 的条件类型在联合类型上是分布式的(distributive conditional types),但由于本题中C固定为一个字面量、Head每次匹配到单一字符,这里表现为一次普通的判断。
  4. 剔除或保留:命中则直接丢弃Head,只递归处理Tail;未命中则把Head保留,拼接到递归结果的前面。
  5. 逐字符累积:递归不断推进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 项目一致:

  1. 在 template.ts 中填入你的类型实现;
  2. 用tsc(仓库 devDependencies 中锁定 TypeScript^5.3.3,见 package.json)对 test-cases.ts 做类型检查,没有报错即代表全部Equal断言通过;
  3. 也可在 tsconfig.json 与 tsconfig.base.json 配置的严格模式下运行,保证解法的类型安全性。

补充一点:本文给出的解法以"每步拆单字符"实现,若想追求更紧凑的写法,也可以利用模板字面量一次匹配更长片段(如S extends${infer A}${C}${infer B}`` 直接按目标字符定位),再对两侧递归,核心原理仍是"模板字面量匹配 + infer 提取 + 条件类型分发 + 递归推进",掌握本文的拆解思路后即可灵活变形。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:终极指南:5分钟掌握ncmdump,轻松解锁网易云NCM加密音乐
下一篇:3分钟快速上手:Windows版Spotify无广告体验完整指南

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

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

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

立即咨询