- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
条件类型(Conditional Types)是 TypeScript 类型系统中"根据输入类型特征做变换"的核心机制,而预定义条件类型则是语言内置的、开箱即用的条件类型工具集合。本文以 The Concise TypeScript Book 的对应章节为主体骨架,系统讲解Exclude、Extract、NonNullable、ReturnType、Parameters、Required、Partial、Readonly共 8 个内置条件类型:先厘清它们各自的语义与底层实现原理,再结合仓库中的类型系统系列章节给出可复制的实战示例与组合用法。读完本文,你将能够熟练使用这些工具类型在编译期完成联合类型过滤、函数签名提取、对象属性修饰符改写等常见类型变换。
一、先理解基础:条件类型从何而来
预定义条件类型并非孤立概念,它们全部建立在 TypeScript 条件类型语法之上。正如仓库中 conditional-types.md 所定义的:条件类型是一种根据条件结果来决定最终类型的机制,使用extends关键字加三元运算符在两种类型间做选择:
type IsArray<T> = T extends any[] ? true : false; const myArray = [1, 2, 3]; const myNumber = 42; type IsMyArrayAnArray = IsArray<typeof myArray>; // Type true type IsMyNumberAnArray = IsArray<typeof myNumber>; // Type false这里T extends any[] ? true : false的含义是:如果T是any[]的子类型,则结果为true,否则为false。可以把extends理解为"子集"关系(T1 ⊆ T2),这与 exploring-the-type-system.md 中"Types as Sets(类型即集合)"的视角一致。
条件类型有两个关键特性,它们是预定义条件类型行为的基石:
- 分配律(Distributive):当
T是联合类型时,条件类型会对每个联合成员分别求值再合并结果,详见 distributive-conditional-types.md:
type Nullable<T> = T extends any ? T | null : never; type NumberOrBool = number | boolean; type NullableNumberOrBool = Nullable<NumberOrBool>; // number | boolean | nullinfer类型推断:条件类型分支中可用infer关键字提取泛型参数的具体类型,详见 infer-type-inference-in-conditional-types.md:
type ElementType<T> = T extends (infer U)[] ? U : never; type Numbers = ElementType<number[]>; // number type Strings = ElementType<string[]>; // string正是"分配律 +infer"这两个能力,构成了Exclude、Extract、ReturnType、Parameters等预定义条件类型的实现底座。
二、联合类型过滤器:Exclude 与 Extract
Exclude与Extract是作用在联合类型上的"正反"两个过滤器,它们的语义在原文档中定义如下:
Exclude<UnionType, ExcludedType>:从UnionType中删除所有可赋值给ExcludedType的类型。Extract<Type, Union>:从Union中提取所有可赋值给Type的类型。
先看一个对照示例:
type Union = 'a' | 'b' | 'c'; type Excluded = Exclude<Union, 'a' | 'c'>; // "b" type Extracted = Extract<Union, 'a' | 'c'>; // "a" | "c"如果把联合类型看作集合,Exclude<T, U>等价于集合差集 T − U,Extract<T, U>等价于集合交集 T ∩ U。它们的典型应用场景包括:
- 剔除事件类型中的某些分支;
- 从对象键集合中排除特定键:
type Keys = 'name' | 'age' | 'email'; type PublicKeys = Exclude<Keys, 'email'>; // "name" | "age" type Falsy = false | 0 | '' | null | undefined; type Truthy = Exclude<any, Falsy>; // 过滤掉所有假值类型Extract的一个实用场景是从接口键中只挑出字符串键或数字键:
interface Article { title: string; views: number; [key: string]: unknown; } type StringKeys = Extract<keyof Article, string>; // string(包含索引签名)仓库中 type-manipulation.md 的 Utility Types 小节同样收录了Exclude与Extract的用法,并给出了与'a' | 'b' | 'c'联合的相同示例,可作为交叉印证。
三、清理空值:NonNullable
NonNullable<Type>的语义非常直接:从Type中移除null和undefined。
type Union = 'a' | null | undefined | 'b'; type MyType = NonNullable<Union>; // 'a' | 'b'该类型最常见的价值在于收窄从 API、表单或第三方库传入的"可能为空"的数据类型。例如 React 中常见的 state 初始化写法:
type User = { id: number; name: string }; // 初始态可能是 null,但后续逻辑保证非空 const initial: User | null = null; type UserState = NonNullable<typeof initial>; // { id: number; name: string }在实际项目中,NonNullable常与可选链、判空分支搭配,让类型层面与运行时空值防御形成"双重保险"。从实现机理看,NonNullable<T>等价于条件类型T extends null | undefined ? never : T:由于条件类型具备分配律,联合类型中的每个成员都会被单独检查,凡是可赋值给null | undefined的成员一律变为never,最终从联合中消失——这也解释了为什么它一次能同时剔除两种空值。
四、提取函数签名:ReturnType 与 Parameters
ReturnType与Parameters是infer关键字在预定义条件类型中最典型的两处应用:
ReturnType<Type>:提取函数Type的返回值类型。Parameters<Type>:提取函数Type的参数类型列表。
type Func = (name: string) => number; type MyType = ReturnType<Func>; // numbertype Func = (a: string, b: number) => void; type MyType = Parameters<Func>; // [a: string, b: number]注意Parameters的返回结果是元组类型(tuple),保留了参数顺序与可选性标记(如[a: string, b?: number])。这在装饰器、高阶函数和事件处理器的类型推导中非常实用:
function makeRequest(url: string, options?: { timeout: number }) {} type RequestParams = Parameters<typeof makeRequest>; // [url: string, options?: { timeout: number } | undefined] type RequestResult = ReturnType<typeof makeRequest>; // void一个真实感很强的组合场景:封装一个包装函数,把被包装函数的参数与返回值全部原样透传:
type Logger<T extends (...args: any[]) => any> = ( ...args: Parameters<T> ) => ReturnType<T>; const add = (a: number, b: number): number => a + b; const loggedAdd: Logger<typeof add> = (a, b) => { console.log(`adding ${a} + ${b}`); return a + b; // 返回值类型被保证为 number };它们的底层原理正是条件类型加infer:ReturnType<T>在分支中通过T extends (...args: any[]) => infer R ? R : any提取R,Parameters<T>则通过T extends (...args: infer P) => any ? P : never提取参数元组P。这与 infer-type-inference-in-conditional-types.md 中ElementType的写法一脉相承。
五、改写对象属性:Required、Partial 与 Readonly
最后三个预定义条件类型作用于对象的属性修饰符,本质上是映射类型(Mapped Types)的语法糖:
Required<Type>:将Type的所有属性变为必填。Partial<Type>:将Type的所有属性变为可选。Readonly<Type>:将Type的所有属性变为只读。
仓库中 mapped-types.md 解释了映射类型的基本形态——用[P in keyof T]遍历对象键并逐个变换属性;mapped-type-modifiers.md 则给出了三个修饰符的底层写法,与这三个工具类型的实现完全对应:
type ReadOnly<T> = { readonly [P in keyof T]: T[P] }; // 等价于 Readonly<T> type Mutable<T> = { -readonly [P in keyof T]: T[P] }; // 去除 readonly type MyPartial<T> = { [P in keyof T]?: T[P] }; // 等价于 Partial<T>5.1 Required:全部属性转必填
type Person = { name?: string; age?: number; }; type A = Required<Person>; // { name: string; age: number; }典型应用是:当配置对象在"初始化阶段"允许缺省、但在"使用阶段"必须完整时,用Required约束后者:
type AppConfig = { host?: string; port?: number; }; // 经过默认值合并后的配置必须是完整的 function startServer(config: Required<AppConfig>) { console.log(`${config.host}:${config.port}`); }Required<T>的底层实现可理解为对每个属性键应用-?修饰符:{ [P in keyof T]-?: T[P] },即去除可选项标记。
5.2 Partial:全部属性转可选
type Person = { name: string; age: number; }; type A = Partial<Person>; // { name?: string | undefined; age?: number | undefined; }Partial最常见的用法是"部分更新"场景——函数的入参只需提供要修改的字段:
type User = { id: number; name: string; email: string; }; function updateUser(id: number, patch: Partial<User>) { // 调用方只需传入需要变更的字段 } updateUser(1, { name: 'new name' }); // 合法 updateUser(1, { id: 2, email: 'a@b.c' }); // 合法注意Partial<T>产生的结果中,可选属性会附带| undefined,因此直接读取可能得到undefined,需要配合判空或默认值处理。
5.3 Readonly:全部属性转只读
type Person = { name: string; age: number; }; type A = Readonly<Person>; const a: A = { name: 'Simon', age: 17 }; a.name = 'John'; // Invalid:只读属性不可赋值Readonly非常适合表达"创建后不可变更"的领域对象或常量配置。需要说明的是,Readonly<T>是浅只读:它只锁定第一层属性,嵌套对象的内部属性仍可修改;对于深层只读,需要递归组合Readonly与索引访问类型,例如type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }。
三个工具类型可以自由嵌套组合,形成更强的类型约束:
type Point = { x?: number; y?: number }; // 先补全必填,再锁定只读 type ImmutablePoint = Readonly<Required<Point>>; // { readonly x: number; readonly y: number; }六、进阶组合与实战技巧
6.1 用 keyof 与 Exclude 动态生成键集合
把keyof(取对象所有键的联合类型)与Exclude/Extract组合,可以在不手写字符串的前提下按条件筛选键:
type FormField = { label: string; value: unknown; hidden?: boolean }; // 剔除可选键,得到必填字段的键集合 type RequiredKeys<T> = Exclude<keyof T, keyof Required<Pick<T, keyof T>>>; type FormRequiredKeys = RequiredKeys<FormField>; // "label" | "value"(hidden 是可选)说明:
RequiredKeys这类高阶工具并非语言内置,而是社区中常见的组合技法;其可行性正建立在Exclude与Required两个内置类型的语义之上。
6.2 函数工具类型的生产级用法
ReturnType与Parameters在依赖注入、事件总线和插件系统里是高频工具。一个经典模式是"从现有函数类型重建一个签名相同的兼容函数":
type Handler = (event: MouseEvent) => void; // 为 handler 增加日志而保持签名完全一致 const safeHandler: Handler = (event) => { console.log('handling', event.type); };如果第三方库没有导出某个函数的参数/返回值类型,Parameters<typeof fn>与ReturnType<typeof fn>可以帮你"凭空提取"出这些类型,无需手写重复声明。
6.3 在仓库中的其他章节定位它们
这些预定义条件类型并非孤立知识点,它们与仓库中多个章节构成完整的知识链条,可交叉阅读:
- conditional-types.md:条件类型语法基础(
extends+ 三元); - distributive-conditional-types.md:分配律如何让
Exclude/Extract/NonNullable对联合逐成员生效; - infer-type-inference-in-conditional-types.md:
ReturnType/Parameters的infer实现原理; - mapped-types.md 与 mapped-type-modifiers.md:
Required/Partial/Readonly的映射底层; - type-manipulation.md:Utility Types 全集,包含
Record、Pick、Omit、Awaited、NoInfer等更多内置工具类型的完整示例; - never-type.md:
never作为"空集合"在条件类型分支(? ... : never)中的角色,以及它在穷尽性检查中的应用。
七、速查总表
| 预定义条件类型 | 语义 | 典型底层写法 | 典型场景 |
|---|---|---|---|
Exclude<T, U> | 从 T 中剔除可赋值给 U 的成员 | T extends U ? never : T | 联合类型过滤、键集合排除 |
Extract<T, U> | 从 T 中提取可赋值给 U 的成员 | T extends U ? T : never | 求交集、按条件筛选键 |
NonNullable<T> | 从 T 中移除null与undefined | T extends null \| undefined ? never : T | 空值收窄、API 数据清洗 |
ReturnType<T> | 提取函数 T 的返回值类型 | T extends (...a: any) => infer R ? R : any | 函数返回值类型复用 |
Parameters<T> | 提取函数 T 的参数元组 | T extends (...a: infer P) => any ? P : never | 参数类型复用、函数包装 |
Required<T> | 所有属性转必填 | { [P in keyof T]-?: T[P] } | 配置完整性校验 |
Partial<T> | 所有属性转可选 | { [P in keyof T]?: T[P] } | 部分更新入参 |
Readonly<T> | 所有属性转只读 | { readonly [P in keyof T]: T[P] } | 不可变对象建模 |
结语
八个预定义条件类型虽然各自解决一个具体问题,但它们的共同点在于:全部由条件类型语法(extends条件判断)、分配律和infer推断三个基础能力派生而来。掌握了它们背后的机理,你不仅能熟练使用这些内置工具,还能以此为模板写出自己的条件类型工具——这正是 TypeScript 类型系统"用类型构造类型"的威力所在。建议对照仓库中的 predefined-conditional-types.md(法文版)或 英文版原文 原文逐条动手验证,并在 IDE 中 hover 观察每个类型求值后的真实结果。
- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
相关推荐
The Concise TypeScript Book:深入理解预定义条件类型(Predefined Conditional Types)
The Concise TypeScript Book:深入理解预定义条件类型(Predefined Conditional Types) 预定义条件类型(Pr
文档教程The Concise TypeScript Book:预定义条件类型(Predefined Conditional Types)深入解析
The Concise TypeScript Book:预定义条件类型(Predefined Conditional Types)深入解析 TypeScript
文档教程The Concise TypeScript Book 精讲:TypeScript 预定义条件类型(Predefined Conditional Types)全解析
The Concise TypeScript Book 精讲:TypeScript 预定义条件类型(Predefined Conditional Types)全
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考