☰
type-challenges 实战:从零实现 TypeScript 内置类型 Exclude<T, U>
2026/9/30 1:59:56 网站建设 项目流程
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

导读

本文以 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>>, ]

三个用例分别覆盖了三种典型场景:

  1. 单个字面量排除:从'a' | 'b' | 'c'中排除'a',期望得到'b' | 'c';
  2. 联合类型排除:从'a' | 'b' | 'c'中排除'a' | 'b',期望得到'c';
  3. 函数类型排除:从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 中关于"题目源自实际遇到的类型问题,不得直接使用内置类型本身"的说明)。该约束的意义在于:

  1. 强迫学习者理解内置类型底层的实现原理,而非黑盒调用;
  2. 帮助建立"条件类型 + 分布式求值 + 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)。验证方式如下:

  1. 本地类型检查:修改 template.ts 中的MyExclude实现,使 test-cases.ts 中的三个Expect断言全部通过;若任一用例不满足Equal断言,TypeScript 编译器会直接报错,无需运行任何测试框架;
  2. 在线挑战:题目入口位于 README.md 中的 "Take the Challenge" 徽章链接,提交后可在线上 Judge 环境获得即时反馈;
  3. 难度递进建议:完成本题后,可继续尝试同仓库中依赖联合类型与条件类型的进阶题,例如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

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:Evolution API缓存策略详解:Redis与本地缓存协同
下一篇:Mac Mouse Fix终极指南:让普通鼠标在macOS上媲美苹果触控板的完整教程

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

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

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

立即咨询