第72课:内置工具类型(下)——Exclude/Extract/NonNullable/Parameters/ReturnType
上一节课我们学习了 Partial、Required、Readonly、Pick、Omit 和 Record,它们主要通过映射类型来变换对象属性的数量和修饰符。本节课将继续探索另一类核心工具类型——它们基于条件类型和 infer 关键字实现,用于在联合类型中过滤成员,或从函数类型中提取参数和返回值的类型信息。理解这些工具的实现原理,是掌握 TypeScript 高级类型编程的关键一步,也是读懂大型项目类型定义和编写泛型工具库的必备技能。
1. 联合类型工具:Exclude、Extract、NonNullable
这三个工具类型用于从联合类型中过滤成员。它们都利用了分布式条件类型的机制——当条件类型作用于裸类型参数且该参数是联合类型时,条件会分布到每个成员上单独求值,然后将结果重新联合。
1.1 Exclude<T, U>:排除成员
Exclude<T, U> 从联合类型 T 中排除所有可以赋值给 U 的成员。
源码级实现:
1 | type Exclude<T, U> = T extends U ? never : T; |
执行过程(分布式):假设 T = 'a' | 'b' | 'c',U = 'a' | 'b'。
'a' extends 'a' | 'b'→true→never'b' extends 'a' | 'b'→true→never'c' extends 'a' | 'b'→false→'c'- 结果:
never | never | 'c'='c'
1 | type Primitive = string | number | boolean | null | undefined | symbol | bigint; |
典型场景:
- 过滤联合类型:从一个大的联合类型中剔除不需要的成员。
- 实现
Omit内部逻辑:Omit<T, K>的底层实现为Pick<T, Exclude<keyof T, K>>(见第 71 课)。 - 限制函数参数:确保传入的参数不是特定类型。
1.2 Extract<T, U>:提取成员
Extract<T, U> 从联合类型 T 中提取所有可以赋值给 U 的成员。它是 Exclude 的反操作。
源码级实现:
1 | type Extract<T, U> = T extends U ? T : never; |
执行过程:与 Exclude 相同,只是 true 分支返回 T(保留),false 分支返回 never(丢弃)。
1 | type AllTypes = string | number | boolean | Date | RegExp; |
典型场景:
- 筛选联合类型中的特定子类型:如从混合类型中提取所有函数、所有对象。
- 获取接口中符合特定值类型的键:结合
keyof和Extract实现“按值类型筛选键”。 - 类型安全的
pick函数:根据某个属性值筛选对象。
1.3 NonNullable<T>:排除 null 和 undefined
NonNullable<T> 从 T 中排除 null 和 undefined。它是 Exclude 的一个特化版本。
源码级实现:
1 | type NonNullable<T> = T extends null | undefined ? never : T; |
等价于 Exclude<T, null | undefined>。
1 | type MaybeString = string | null | undefined; |
典型场景:
- 清洗可选参数:从
T | null | undefined中提取“确定有值”的类型。 - 与
Parameters结合:提取函数参数后排除 undefined 类型。 - 在类型守卫中配合使用:确保被检查后的值的精确类型。
1.4 三者的对比与联系
| 工具类型 | 操作 | 公式 | 结果 |
|---|---|---|---|
Exclude<T, U> |
排除 U 的成员 | T extends U ? never : T |
T 中可赋值给 U 的成员被移除。 |
Extract<T, U> |
提取 U 的成员 | T extends U ? T : never |
仅保留 T 中可赋值给 U 的成员。 |
NonNullable<T> |
排除 null/undefined | T extends null | undefined ? never : T |
移除空类型。 |
2. 函数类型工具:Parameters、ReturnType、ThisParameterType、OmitThisParameter
这四个工具类型用于从函数类型中提取内部信息。它们都依赖于 infer 关键字——在条件类型中声明待推断的类型变量,从函数签名中解构出参数元组和返回值类型。
2.1 Parameters<T>:提取函数参数元组
Parameters<T> 提取函数类型 T 的参数类型,返回一个元组类型。
源码级实现:
1 | type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; |
T extends (...args: any) => any约束T必须是一个函数类型。(...args: infer P) => any使用infer P推断函数参数列表的类型。- 若匹配成功,返回推断出的元组
P。
1 | function createUser(name: string, age: number, isAdmin: boolean): User { |
典型场景:
- 提取函数参数类型:在编写高阶函数时,获取被包装函数的参数类型。
- React 组件 Props 类型复用:从组件函数中提取 Props 类型,避免重复定义。
- 编写类型安全的工具函数:如
debounce、throttle保留原函数的参数类型。
2.2 ReturnType<T>:提取函数返回值类型
ReturnType<T> 提取函数类型 T 的返回值类型。
源码级实现:
1 | type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never; |
1 | function fetchUser(): Promise<{ id: number; name: string }> { |
典型场景:
- 提取异步函数返回值:获取 Promise 中包裹的类型。
- 获取工厂函数的产出类型:在创建第三方库的实例时,推断实例的类型。
- 编写高阶函数:确保包装函数的返回值类型与原始函数一致。
2.3 ThisParameterType<T>:提取函数的 this 类型
ThisParameterType<T> 提取函数类型 T 中声明的 this 参数类型(如果存在)。
源码级实现:
1 | type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any ? U : unknown; |
1 | function showDate(this: Date) { |
2.4 OmitThisParameter<T>:移除函数的 this 参数
OmitThisParameter<T> 将带有 this 参数的函数类型转换为不带有 this 参数的普通函数类型。
源码级实现:
1 | type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T; |
1 | function greet(this: { name: string }, greeting: string): string { |
3. 综合应用:编写一个类型安全的 debounce 函数
我们将综合运用 Parameters 和 ReturnType,实现一个保留原函数类型签名的 debounce 工具函数。
1 | function debounce<T extends (...args: any) => any>( |
类型拆解:
T extends (...args: any) => any约束T为函数。Parameters<T>在返回函数中复用原函数的参数类型。ReturnType<typeof setTimeout>获取setTimeout返回值的类型(Node 环境下为NodeJS.Timeout,浏览器为number)。
4. 最佳实践与常见陷阱
4.1 避免在简单场景中过度使用工具类型
Exclude、Extract 等工具类型非常灵活,但如果类型本身很简单(如只包含 2-3 个成员的联合类型),直接写联合类型更清晰,过度封装反而降低可读性。
4.2 注意分布式条件类型的触发条件
Exclude 和 Extract 依赖分布式条件类型。如果类型参数被包裹在元组或对象中,分布行为会失效。这是很多人对 Exclude 行为感到困惑的根源。
4.3 Parameters 和 ReturnType 不支持函数重载
Parameters 和 ReturnType 只能提取最后一个函数签名的参数和返回值类型。对于重载函数,需要考虑其他方案(如定义类型别名覆盖不同重载)。
4.4 类型工具链的组合使用
实际项目中,这些工具类型经常被链式组合:
Partial<Pick<T, keyof T>>→ 所有属性可选(同Partial<T>)。Parameters<typeof fn>[0]→ 提取第一个参数的类型。ReturnType<typeof fn> extends Promise<infer R> ? R : never→ 提取异步函数返回的 Promise 的包裹类型。
课后练习
一、概念自测(选择题 / 填空题)
(单选)
Exclude<string | number | boolean, string | boolean>的结果是?
A.string | boolean
B.number
C.string | number | boolean
D.never(单选)
NonNullable<T>在内部是如何实现的?
A.T extends null ? never : T
B.T extends undefined ? T : never
C.T extends null | undefined ? never : T
D.T & {}(填空) 要提取函数
fn的参数类型元组,应使用______<typeof fn>。(多选) 以下哪些工具类型使用了
infer关键字?
A.Exclude<T, U>
B.ReturnType<T>
C.Parameters<T>
D.OmitThisParameter<T>
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 TypeScript 高阶函数 withLoading<T extends (...args: any) => any>(fn: T),它接收一个异步函数 fn,返回一个新函数。新函数的参数类型与 fn 完全相同,但返回值类型变为 { loading: boolean; data: Awaited<ReturnType<T>> | null; error: Error | null }。要求如下:
- 使用
Parameters<T>保留原函数的参数类型。 - 使用
ReturnType<T>和Awaited(TS 4.5+ 内置)获取原函数 Promise 包裹的值类型。 - 返回的新函数内部调用
fn,管理 loading 状态,捕获错误。 - 添加 JSDoc 注释说明泛型参数、输入和输出类型。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 代码。提示词中需明确指定工具类型的使用、高阶函数的类型签名和状态管理逻辑。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建 TypeScript 文件
src/withLoading.ts,实现一个高阶函数withLoading。要求:
- 函数签名:
function withLoading<T extends (...args: any) => Promise<any>>(fn: T): (...args: Parameters<T>) => { loading: boolean; data: Awaited<ReturnType<T>> | null; error: Error | null }。- 内部使用
let state = { loading: false, data: null, error: null }。返回一个async函数,在执行fn前设置loading = true,成功后设置data,失败设置error,finally 设置loading = false,并返回state对象。- 使用
const声明withLoading,为函数添加 JSDoc 注释。- 在文件末尾添加测试示例:定义一个
async function fetchUser(id: number): Promise<{ name: string }>,用withLoading包装,并注释说明类型正确。- 使用 ESM 导出函数,确保代码在严格 TypeScript 下可编译。完成后列出完整文件内容。
四、面试真题与参考答案
题目(字节跳动前端面试题):
请解释 TypeScript 中
infer关键字的作用,并结合ReturnType和Parameters的实现原理说明它的典型使用场景。
参考答案:
infer 关键字只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量。当条件类型匹配成功时,TypeScript 会在这个位置推断出具体的类型,并可在 true 分支中使用。它本质上是“类型层面的模式匹配与解构”。
ReturnType<T> 的实现 T extends (...args: any) => infer R ? R : never 中,infer R 匹配函数的返回值类型。Parameters<T> 的实现 T extends (...args: infer P) => any ? P : never 中,infer P 匹配函数的参数元组类型。
infer 的典型使用场景:
- 从函数类型中提取参数和返回值:
Parameters、ReturnType。 - 提取 Promise 包裹的类型:
T extends Promise<infer R> ? R : never。 - 提取数组元素类型:
T extends (infer U)[] ? U : never。 - 提取嵌套结构的内部类型:如从
React.ComponentProps中提取组件的 Props。
infer 的核心价值在于:它使得类型系统具备了“解构”复杂类型的能力,将类型编程从“描述”提升到“分析和推导”的层次。
课后练习答案
一、概念自测答案
B
- 解析:
Exclude<string | number | boolean, string | boolean>分布到每个成员上——string被排除(never),boolean被排除(never),number保留,结果为number。
- 解析:
C
- 解析:
NonNullable<T>通过分布式条件类型T extends null | undefined ? never : T排除null和undefined。
- 解析:
Parameters- 解析:
Parameters<typeof fn>提取函数的参数元组类型。
- 解析:
B、C、D
- 解析:
ReturnType使用infer R推断返回值,Parameters使用infer P推断参数元组,OmitThisParameter内部也使用了infer。Exclude仅使用分布式条件类型,不使用infer。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请用 TypeScript 编写一个高阶函数withLoading(fn),接收一个返回 Promise 的函数,返回包装后的函数。要求:
- 函数签名:
function withLoading<T extends (...args: any) => Promise<any>>(fn: T): (...args: Parameters<T>) => Promise<{ loading: boolean; data: Awaited<ReturnType<T>> | null; error: Error | null }>。- 包装函数内部创建
state对象(loading: false, data: null, error: null),try执行fn(...args)更新data,catch更新error,finally更新loading并返回state。- 添加 JSDoc 注释。
- 提供测试示例:
async function fetchUser(id: number): Promise<{ name: string }>,用withLoading包装并验证类型推断。- 使用
const声明,输出完整代码。”