WinddSnow

TypeScript-Built-in-Utility-Types-Part2-Exclude-Extract-NonNullable-Parameter...

字数统计: 3.5k阅读时长: 15 min
2026/07/31

第72课:内置工具类型(下)——Exclude/Extract/NonNullable/Parameters/ReturnType

上一节课我们学习了 PartialRequiredReadonlyPickOmitRecord,它们主要通过映射类型来变换对象属性的数量和修饰符。本节课将继续探索另一类核心工具类型——它们基于条件类型infer 关键字实现,用于在联合类型中过滤成员,或从函数类型中提取参数和返回值的类型信息。理解这些工具的实现原理,是掌握 TypeScript 高级类型编程的关键一步,也是读懂大型项目类型定义和编写泛型工具库的必备技能。


1. 联合类型工具:ExcludeExtractNonNullable

这三个工具类型用于从联合类型中过滤成员。它们都利用了分布式条件类型的机制——当条件类型作用于裸类型参数且该参数是联合类型时,条件会分布到每个成员上单独求值,然后将结果重新联合。

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'truenever
  • 'b' extends 'a' | 'b'truenever
  • 'c' extends 'a' | 'b'false'c'
  • 结果:never | never | 'c' = 'c'
1
2
3
4
5
6
7
8
9
10
type Primitive = string | number | boolean | null | undefined | symbol | bigint;

// 从 Primitive 中排除 null 和 undefined,得到“非空原始类型”
type NonNullPrimitive = Exclude<Primitive, null | undefined>;
// string | number | boolean | symbol | bigint

// 从字符串联合中排除特定值
type AllowedEvents = 'click' | 'focus' | 'blur' | 'keydown' | 'keyup';
type MouseEvents = Exclude<AllowedEvents, 'focus' | 'blur' | 'keydown' | 'keyup'>;
// 'click'

典型场景

  • 过滤联合类型:从一个大的联合类型中剔除不需要的成员。
  • 实现 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
2
3
4
5
6
7
8
9
10
type AllTypes = string | number | boolean | Date | RegExp;

// 提取函数类型(这里 Date 和 RegExp 都是对象类型,可用 object 约束)
type ObjectTypes = Extract<AllTypes, object>;
// Date | RegExp

// 提取特定字符串
type HttpMethods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ReadMethods = Extract<HttpMethods, 'GET' | 'HEAD' | 'OPTIONS'>;
// 'GET'

典型场景

  • 筛选联合类型中的特定子类型:如从混合类型中提取所有函数、所有对象。
  • 获取接口中符合特定值类型的键:结合 keyofExtract 实现“按值类型筛选键”。
  • 类型安全的 pick 函数:根据某个属性值筛选对象。

1.3 NonNullable<T>:排除 nullundefined

NonNullable<T>T 中排除 nullundefined。它是 Exclude 的一个特化版本。

源码级实现

1
type NonNullable<T> = T extends null | undefined ? never : T;

等价于 Exclude<T, null | undefined>

1
2
3
4
5
type MaybeString = string | null | undefined;
type SafeString = NonNullable<MaybeString>; // string

type UserInput = string | number | null;
type ValidInput = NonNullable<UserInput>; // string | number

典型场景

  • 清洗可选参数:从 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. 函数类型工具:ParametersReturnTypeThisParameterTypeOmitThisParameter

这四个工具类型用于从函数类型中提取内部信息。它们都依赖于 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
2
3
4
5
6
7
8
9
10
11
12
13
function createUser(name: string, age: number, isAdmin: boolean): User {
return { name, age, isAdmin };
}

type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number, isAdmin: boolean]

// 可用于 React 组件的 Props 提取
function MyComponent(props: { title: string; count: number }) {
return null;
}
type MyComponentProps = Parameters<typeof MyComponent>[0];
// { title: string; count: number }

典型场景

  • 提取函数参数类型:在编写高阶函数时,获取被包装函数的参数类型。
  • React 组件 Props 类型复用:从组件函数中提取 Props 类型,避免重复定义。
  • 编写类型安全的工具函数:如 debouncethrottle 保留原函数的参数类型。

2.2 ReturnType<T>:提取函数返回值类型

ReturnType<T> 提取函数类型 T返回值类型

源码级实现

1
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;
1
2
3
4
5
6
7
8
9
10
11
function fetchUser(): Promise<{ id: number; name: string }> {
return fetch('/api/user').then(r => r.json());
}

type UserPromise = ReturnType<typeof fetchUser>; // Promise<{ id: number; name: string }>

// 提取类方法返回值
class Calculator {
add(a: number, b: number): number { return a + b; }
}
type AddReturn = ReturnType<Calculator['add']>; // number

典型场景

  • 提取异步函数返回值:获取 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
2
3
4
5
function showDate(this: Date) {
return this.toISOString();
}

type ThisType = ThisParameterType<typeof showDate>; // 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
2
3
4
5
function greet(this: { name: string }, greeting: string): string {
return `${greeting}, ${this.name}`;
}

type PureGreet = OmitThisParameter<typeof greet>; // (greeting: string) => string

3. 综合应用:编写一个类型安全的 debounce 函数

我们将综合运用 ParametersReturnType,实现一个保留原函数类型签名的 debounce 工具函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function debounce<T extends (...args: any) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}

// 测试
const log = (message: string, count: number) => {
console.log(message, count);
};

const debouncedLog = debounce(log, 300);
debouncedLog('Hello', 42); // 类型安全:参数类型与 log 一致

类型拆解

  • T extends (...args: any) => any 约束 T 为函数。
  • Parameters<T> 在返回函数中复用原函数的参数类型。
  • ReturnType<typeof setTimeout> 获取 setTimeout 返回值的类型(Node 环境下为 NodeJS.Timeout,浏览器为 number)。

4. 最佳实践与常见陷阱

4.1 避免在简单场景中过度使用工具类型

ExcludeExtract 等工具类型非常灵活,但如果类型本身很简单(如只包含 2-3 个成员的联合类型),直接写联合类型更清晰,过度封装反而降低可读性。

4.2 注意分布式条件类型的触发条件

ExcludeExtract 依赖分布式条件类型。如果类型参数被包裹在元组或对象中,分布行为会失效。这是很多人对 Exclude 行为感到困惑的根源。

4.3 ParametersReturnType 不支持函数重载

ParametersReturnType 只能提取最后一个函数签名的参数和返回值类型。对于重载函数,需要考虑其他方案(如定义类型别名覆盖不同重载)。

4.4 类型工具链的组合使用

实际项目中,这些工具类型经常被链式组合:

  • Partial<Pick<T, keyof T>> → 所有属性可选(同 Partial<T>)。
  • Parameters<typeof fn>[0] → 提取第一个参数的类型。
  • ReturnType<typeof fn> extends Promise<infer R> ? R : never → 提取异步函数返回的 Promise 的包裹类型。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) Exclude<string | number | boolean, string | boolean> 的结果是?
    A. string | boolean
    B. number
    C. string | number | boolean
    D. never

  2. (单选) NonNullable<T> 在内部是如何实现的?
    A. T extends null ? never : T
    B. T extends undefined ? T : never
    C. T extends null | undefined ? never : T
    D. T & {}

  3. (填空) 要提取函数 fn 的参数类型元组,应使用 ______<typeof fn>

  4. (多选) 以下哪些工具类型使用了 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 关键字的作用,并结合 ReturnTypeParameters 的实现原理说明它的典型使用场景。

参考答案

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 的典型使用场景:

  • 从函数类型中提取参数和返回值ParametersReturnType
  • 提取 Promise 包裹的类型T extends Promise<infer R> ? R : never
  • 提取数组元素类型T extends (infer U)[] ? U : never
  • 提取嵌套结构的内部类型:如从 React.ComponentProps 中提取组件的 Props。

infer 的核心价值在于:它使得类型系统具备了“解构”复杂类型的能力,将类型编程从“描述”提升到“分析和推导”的层次。


课后练习答案

一、概念自测答案

  1. B

    • 解析:Exclude<string | number | boolean, string | boolean> 分布到每个成员上——string 被排除(never),boolean 被排除(never),number 保留,结果为 number
  2. C

    • 解析:NonNullable<T> 通过分布式条件类型 T extends null | undefined ? never : T 排除 nullundefined
  3. Parameters

    • 解析:Parameters<typeof fn> 提取函数的参数元组类型。
  4. B、C、D

    • 解析:ReturnType 使用 infer R 推断返回值,Parameters 使用 infer P 推断参数元组,OmitThisParameter 内部也使用了 inferExclude 仅使用分布式条件类型,不使用 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) 更新 datacatch 更新 errorfinally 更新 loading 并返回 state
  • 添加 JSDoc 注释。
  • 提供测试示例:async function fetchUser(id: number): Promise<{ name: string }>,用 withLoading 包装并验证类型推断。
  • 使用 const 声明,输出完整代码。”
CATALOG
  1. 1. 第72课:内置工具类型(下)——Exclude/Extract/NonNullable/Parameters/ReturnType
    1. 1.1. 1. 联合类型工具:Exclude、Extract、NonNullable
      1. 1.1.1. 1.1 Exclude<T, U>:排除成员
      2. 1.1.2. 1.2 Extract<T, U>:提取成员
      3. 1.1.3. 1.3 NonNullable<T>:排除 null 和 undefined
      4. 1.1.4. 1.4 三者的对比与联系
    2. 1.2. 2. 函数类型工具:Parameters、ReturnType、ThisParameterType、OmitThisParameter
      1. 1.2.1. 2.1 Parameters<T>:提取函数参数元组
      2. 1.2.2. 2.2 ReturnType<T>:提取函数返回值类型
      3. 1.2.3. 2.3 ThisParameterType<T>:提取函数的 this 类型
      4. 1.2.4. 2.4 OmitThisParameter<T>:移除函数的 this 参数
    3. 1.3. 3. 综合应用:编写一个类型安全的 debounce 函数
    4. 1.4. 4. 最佳实践与常见陷阱
      1. 1.4.1. 4.1 避免在简单场景中过度使用工具类型
      2. 1.4.2. 4.2 注意分布式条件类型的触发条件
      3. 1.4.3. 4.3 Parameters 和 ReturnType 不支持函数重载
      4. 1.4.4. 4.4 类型工具链的组合使用
    5. 1.5. 课后练习
      1. 1.5.1. 一、概念自测(选择题 / 填空题)
      2. 1.5.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.5.3. 三、Agent 模式下的提示词示例
      4. 1.5.4. 四、面试真题与参考答案
    6. 1.6. 课后练习答案
      1. 1.6.1. 一、概念自测答案
      2. 1.6.2. 二、AI 编程任务参考答案(提示词示例)