首页 / TypeScript 入门教程 / 内置工具类型(下)

TypeScript 入门教程

内置工具类型(下)

本教程共 80 篇 · 第 42 篇 · 更新于 2026-08-10 · 约 12 分钟阅读

TypeScriptTypeScript 入门教程工具类型条件类型

本节目标:掌握 RecordExcludeExtractNonNullableReturnTypeParametersAwaited——它们的实现涉及条件类型和 infer,是 TypeScript 类型体操的”基础招式”。

上一章的工具类型全部基于映射类型实现,没有用到条件判断。这一章的七个工具类型要么用了条件类型,要么用到了更复杂的组合——它们能处理的场景也更”元编程”。

Record<Keys, Type>:批量构造对象类型

它能干什么

Record<Keys, Type> 构造一个对象类型,键名来自 Keys(必须是 string | number | symbol 的子类型),值类型固定为 Type

type CatName = "miffy" | "boris" | "mordred";

interface CatInfo {
  age: number;
  breed: string;
}

// 构造一个"猫名 → 猫信息"的映射类型
const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};

cats.boris.age; // number ✅

使用 Record 的一个前提:你必须覆盖所有键。上面的例子中,如果少填了 "mordred",编译器会立刻报错。

手写实现原理

type MyRecord<K extends string | number | symbol, T> = {
  [P in K]: T;
};

就是映射类型的最简形式——遍历 K 的每个成员,值类型统一为 T。跟前面 Partial 的区别在于:映射的键来源不是 keyof 某个类型,而是一个联合类型 K

Exclude<UnionType, ExcludedMembers>:从联合中排除

它能干什么

Exclude<UnionType, ExcludedMembers> 从第一个联合类型中剔除能赋值给第二个参数的所有成员:

type T0 = Exclude<"a" | "b" | "c", "a">;
// type T0 = "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// type T1 = "c"

type T2 = Exclude<string | number | (() => void), Function>;
// type T2 = string | number

第三个例子值得留意——(() => void)Function 的子类型,所以被排除,剩下 string | number

手写实现原理

type MyExclude<T, U> = T extends U ? never : T;

这里用到了条件类型的**分布(distributive)**特性。当 T 是联合类型时,T extends U ? never : T 会被分配到联合的每一个成员上:

Exclude<"a" | "b" | "c", "a">
→ ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b") | ("c" extends "a" ? never : "c")
→ never | "b" | "c"
→ "b" | "c"    (never 在联合中自动消失)
Note

never 在联合类型中会被自动吸收——就像数学里的”空集”。任何类型跟 never 做联合,结果还是那个类型本身。

Extract<Type, Union>:从联合中提取

它能干什么

Extract<Type, Union>Exclude 的反向——只保留能赋值给第二个参数的那些成员:

type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// type T0 = "a"   ——"a" 是交集,"b" 和 "c" 都不在右边

type T1 = Extract<string | number | (() => void), Function>;
// type T1 = () => void   ——只有函数类型是 Function 的子类型

手写实现原理

type MyExtract<T, U> = T extends U ? T : never;

Exclude 的区别只是把 never : T 换成了 T : neverExclude 是在”匹配时抛弃”,Extract 是在”匹配时保留”。

NonNullable<Type>:排除 null 和 undefined

它能干什么

NonNullable<Type> 从联合类型中排除 nullundefined

type T0 = NonNullable<string | number | undefined>;
// type T0 = string | number

type T1 = NonNullable<string[] | null | undefined>;
// type T1 = string[]

type T2 = NonNullable<string | null>;
// type T2 = string

非常实用的场景:处理后端返回的”可能为空”的数据时,用 NonNullable 做一层类型收窄。

手写实现原理

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

就是 Exclude 的具体化——把 U 固定为 null | undefined

ReturnType<Type>:取函数返回值类型

它能干什么

ReturnType<Type> 从函数类型中提取返回值类型:

function greet(name: string): string {
  return `你好,${name}`;
}

type GreetReturn = ReturnType<typeof greet>;
// type GreetReturn = string

type T0 = ReturnType<() => string>;      // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>;          // unknown
Note

对于重载函数,ReturnType 取的是最后一个重载签名的返回值类型——也就是最宽泛的那一个。

手写实现原理

type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;

这里出现了新关键词 infer。它的作用是:在条件类型的 extends 分支中声明一个临时的类型变量,让 TypeScript 去推断它。

拆解一下:

  • T extends (...args: any) => infer R:如果 T 是一个函数类型,就把它的返回值类型捕获到变量 R 里。
  • ? R : never:如果能匹配,返回 R;否则返回 never

infer 是 TypeScript 类型系统中的”反推”能力——你不用手动把函数签名拆开去读返回值,infer 帮你做这件事。

Parameters<Type>:取函数参数元组

它能干什么

Parameters<Type> 从函数类型中提取参数列表,结果是一个元组类型:

function greet(name: string, age: number): string {
  return `${name} 今年 ${age} 岁`;
}

type GreetParams = Parameters<typeof greet>;
// type GreetParams = [name: string, age: number]

type T0 = Parameters<() => string>;           // []
type T1 = Parameters<(s: string) => void>;    // [s: string]
type T2 = Parameters<<T>(arg: T) => T>;       // [arg: unknown]

手写实现原理

type MyParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

ReturnType 思路一致,只是 infer 的位置不同:

  • ReturnType 在返回值位置 infer R
  • Parameters 在参数位置 infer P

Awaited<Type>:递归解包 Promise

它能干什么

Awaited<Type> 模拟 await 的行为——递归解开 Promise 包裹,直到拿到最内层的值类型:

type A = Awaited<Promise<string>>;
// type A = string

type B = Awaited<Promise<Promise<number>>>;
// type B = number    ——两层 Promise,全部解包

type C = Awaited<boolean | Promise<number>>;
// type C = boolean | number

手写实现原理

type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;

关键点:

  • T extends Promise<infer U>:如果 TPromise<U>,提取内层类型 U
  • ? MyAwaited<U>递归调用自己——如果 U 又是一个 Promise,那就继续解包。
  • : T:如果不是 Promise,直接返回 T

注意:TypeScript 的类型递归有限制,但 Awaited 这种”一层层剥开 Promise”的递归是官方支持的。

小结

这七个工具类型的实现本质是一句话:条件类型 T extends U ? X : Y 配合 infer 做模式匹配

工具类型核心机制关键词
Record<K, T>映射类型
Exclude<T, U>分布式条件extends + never
Extract<T, U>分布式条件extends + T
NonNullable<T>分布式条件extends null|undefined
ReturnType<T>条件类型 + inferinfer R
Parameters<T>条件类型 + inferinfer P
Awaited<T>递归 + inferinfer U + 递归

infer 是理解后三个工具类型的关键——它让你在”模式匹配”的过程中从类型里抓出想要的片段。下一章我们正式深入条件类型,看看分布特性和 never 过滤到底是怎么工作的。