- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
导读
本文以 type-challenges 题库第 00043 题(Exclude)的日文/中文/英文题面为骨架,讲解如何在不借助内置Exclude<T, U>的情况下,用条件类型与分布式行为实现"从联合类型T中排除可赋值给U的类型"。读完本文,你将掌握联合类型、条件类型分布式求值的底层原理,并能够独立通过仓库中的类型级测试用例,为后续处理Omit、Pick等联合类型操作打下基础。
题目背景:内置类型 Exclude 是什么
Exclude<T, U>是 TypeScript 标准库中的一个内置类型工具,其作用正如仓库中 README.ja.md(日文题面)与 README.zh-CN.md(中文题面)所述:
从联合类型
T中排除U中可赋值的类型,来构造一个新的类型(原文档英文原文:Exclude fromTthose types that are assignable toU)。
题目给出的示例为:
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'也就是说,题目要求我们自己实现一个名为MyExclude的类型,它的行为必须与内置的Exclude<T, U>完全一致,但不允许直接使用内置Exclude本身。这是一道标记为easy(初級)难度、标签为built-in(内置类型)与union(联合类型)的基础题,其元数据可在 info.yml 中查看。
准备工作:仓库中的模板与测试
在动手实现之前,先了解本仓库为该题提供的两个关键文件:
起点模板(template.ts)
templates.ts 只给出一个占位定义,类型被写死为any,等待我们替换为真正的实现:
type MyExclude<T, U> = any类型级测试(test-cases.ts)
test-cases.ts 是本题的类型级验证用例,通过@type-challenges/utils中导出的Equal与Expect工具进行编译期断言:
import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>, Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a' | 'b'>, 'c'>>, Expect<Equal<MyExclude<string | number | (() => void), Function>, string | number>>, ]三个用例分别覆盖了三种典型场景:
- 单个字面量排除:从
'a' | 'b' | 'c'中排除'a',期望得到'b' | 'c'; - 联合类型排除:从
'a' | 'b' | 'c'中排除'a' | 'b',期望得到'c'; - 函数类型排除:从
string | number | (() => void)中排除Function,期望得到string | number(因为() => void可赋值给Function,从而被剔除)。
这三个用例是判断实现是否正确的唯一标准:只要MyExclude满足上述三条断言,就等价于内置Exclude。
测试工具Equal的实现位于 utils/index.d.ts,它通过比较两个函数类型在泛型下的返回值是否一致来严格判断X与Y是否完全相同:
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false这意味着我们不能用any、unknown等宽泛类型"蒙混过关",MyExclude的结果必须与期望类型精确相等。
核心解法:利用条件类型的分布式求值
分布式条件类型(Distributive Conditional Types)
要理解解法,首先要理解 TypeScript 条件类型的一个重要特性:当T extends U ? X : Y中的T是裸类型参数(naked type parameter)且被传入一个联合类型时,条件类型会被分发(distribute)到联合类型的每一个成员上。
也就是说,对于T = 'a' | 'b' | 'c',条件类型:
T extends U ? X : Y会被展开为:
('a' extends U ? X : Y) | ('b' extends U ? X : Y) | ('c' extends U ? X : Y)这正是实现Exclude的基石:对联合类型的每个成员分别判断,再重新组合成新的联合类型。
实现 MyExclude
利用分布式条件类型,只需一行即可完成实现:
type MyExclude<T, U> = T extends U ? never : T逐项拆解其执行过程:
- 当
T = 'a' | 'b' | 'c'、U = 'a'时,条件类型分发为三个独立的判断:'a' extends 'a' ? never : 'a'(结果为never)、'b' extends 'a' ? never : 'b'(结果为'b')、'c' extends 'a' ? never : 'c'(结果为'c'),最终合并为'b' | 'c',与题目示例一致; - 当
U = 'a' | 'b'时,'a'与'b'均命中extends U分支返回never,只剩'c'; - 当
T = string | number | (() => void)、U = Function时,由于() => void可赋值给Function,该成员被剔除,结果为string | number。
这里never的选择非常关键:在联合类型中,never会被自动吸收(即'a' | never等价于'a'),因此被排除的成员不会残留任何痕迹。这也是"排除(Exclude)"语义在类型层面的自然映射。
为什么不直接写Exclude<T, U>
题目明确要求不使用内置的Exclude<T, U>,这是 type-challenges 系列题目的通用规则(见本仓库 README.zh-CN.md 中关于"题目源自实际遇到的类型问题,不得直接使用内置类型本身"的说明)。该约束的意义在于:
- 强迫学习者理解内置类型底层的实现原理,而非黑盒调用;
- 帮助建立"条件类型 + 分布式求值 + never 吸收"的类型思维,这套组合拳在后续
MyOmit、MyPick、Exclude的兄弟题(如Omit、RequiredKeys、OptionalKeys)中会反复出现。
进阶:为什么第一个参数必须是"裸类型参数"
分布式求值有一个容易踩坑的前提:被分发的类型参数必须是裸类型参数。如果T被包裹在类型运算中(例如T[]、Readonly<T>、(T) => void),条件类型将不再分发,而是对整体进行一次性判断。
我们可以做一个反例验证。若把实现写成:
// 错误示范:T 不是裸类型参数,不会分发 type WrongExclude<T, U> = T[] extends U[] ? never : T当传入'a' | 'b' | 'c'时,T被整体视为('a' | 'b' | 'c')[]参与判断,而不是对每个成员分别判断,结果与期望完全不符。这也是为什么MyExclude的实现必须保持T extends U ? never : T这样直接使用T的形态。
顺带一提,仓库 utils 中的UnionToIntersection(utils/index.d.ts)正是反向利用了这个特性——通过U extends any ? (k: U) => void : never强制分发后再求交集,可见分布式条件类型在高级类型编程中的核心地位。
如何在仓库中验证你的解答
本仓库采用 pnpm workspace 管理,@type-challenges/utils作为工作区包被引用(见根目录 package.json 与 utils/package.json)。验证方式如下:
- 本地类型检查:修改 template.ts 中的
MyExclude实现,使 test-cases.ts 中的三个Expect断言全部通过;若任一用例不满足Equal断言,TypeScript 编译器会直接报错,无需运行任何测试框架; - 在线挑战:题目入口位于 README.md 中的 "Take the Challenge" 徽章链接,提交后可在线上 Judge 环境获得即时反馈;
- 难度递进建议:完成本题后,可继续尝试同仓库中依赖联合类型与条件类型的进阶题,例如
Omit(00003-medium-omit)、ReadonlyKeys等,进一步巩固分布式条件类型的应用。
小结
本文从 type-challenges 第 00043 题出发,完成了从题目解读、测试用例分析到源码实现的全过程:
Exclude<T, U>的语义是从T中排除所有可赋值给U的联合成员;- 标准实现
type MyExclude<T, U> = T extends U ? never : T借助分布式条件类型逐个成员判断,再用never的联合吸收特性剔除目标成员; - 验证标准是仓库中 test-cases.ts 的三条
Equal断言,它们覆盖了单字面量排除、联合排除和函数类型排除三种场景。
掌握了这一定义,你就掌握了 TypeScript 联合类型运算中最基础、最常用的一块拼图。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
实现内置 `Exclude<T, U>`:从联合类型中排除指定成员(type-challenges 第 43 题)
实现内置 Exclude<T, U :从联合类型中排除指定成员(type challenges 第 43 题) 本指南围绕 type challenges 仓库
示例工程SANA 安装指南:从零搭建环境到 Diffusers 快速推理实战
SANA 安装指南:从零搭建环境到 Diffusers 快速推理实战 本指南是 SANA(Efficient High Resolution Image Syn
示例工程从零实现 TypeScript 内置 Pick 工具类型:type-challenges 第 4 题完整实战解析
从零实现 TypeScript 内置 Pick 工具类型:type challenges 第 4 题完整实战解析 导读 本文基于 type challenges
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考