第五章:解锁类型系统的高级魔法 - 高级泛型
第五章:解锁类型系统的高级魔法 - 高级泛型
Prorise第五章:解锁类型系统的高级魔法 - 高级泛型
摘要: 欢迎来到第五章。在本章,我们的思维将再次跃迁——从“使用类型”到“编程 类型”。我们将直面 Todo 应用在功能迭代(如“部分更新”)中遇到的新瓶颈,并引入 TypeScript 的高级武器库:映射类型 与 条件类型。您将学会如何像炼金术士一样,对现有类型进行裁剪 (Pick
/Omit
)、转换 (Partial
/Required
) 和逻辑判断 (extends
/infer
),创造出全新的、精确的类型。这不仅是技巧的学习,更是理解 Vue/React 等现代框架背后“类型魔法”的关键一步。
注意: 接下来的章节难度会稍有提高,我还是会尽量的秉承我们的讲解风格,但能否听懂这章节以及听懂这章节的用处在乎你如果是一个追求高层次的 Typescript 学习者,还是你只是想在项目中使用 Typescript ,那么这就是一道分水岭,如果你认为后续的章节太难,而又想过度到 Vue3 的话,没问题,后续的章节对您的帮助都不会太大
5.1. 痛点呈现:如何优雅地处理“部分更新”
随着我们的 Todo 应用变得越来越真实,一个核心需求出现了:更新一个已存在的待办事项。让我们看看在当前知识体系下,实现这个功能会遇到什么尴尬的局面。
第一步:在我们的 types.ts
中添加 AppState
为了代码的清晰,我们将整个应用的状态也模型化。
文件路径: src/types.ts
1 | // 确保存在如下的状态模型 |
第二步:在 main.ts
中添加 updateTodo
函数的“天真”实现
文件路径: src/main.ts
1 | import { Todo, AppState, Filter } from './types'; // 引入 AppState |
第三步:暴露痛点
现在,假设我们只想将一个 Todo 的状态切换为“已完成”,我们该如何调用 updateTodo
?
1 | // 场景:只想把 id 为 2 的 todo 标记为完成 |
核心问题: 函数的类型签名 (updatedTodo: Todo
) 过于严格,缺乏灵活性。它要求我们为了修改一小部分数据,而兴师动众地提供一个完整的数据结构。这在工程实践中是繁琐且低效的。
5.2. 映射类型:类型的“批量转换”
为了解决上述痛点,TypeScript 提供了一种强大的元编程能力——映射类型。它允许我们基于一个现有类型,通过某种规则“批量转换”出另一个新类型。
5.2.1. Partial<T>
: 完美的更新载荷 (Payload)
Partial<T>
是 TypeScript 内置的一个映射类型,它的作用是将类型 T
的所有属性都变为 可选的。
第四步:使用 Partial<T>
重构 updateTodo
文件路径: src/main.ts
(修改 updateTodo
函数)
1 | // ... 其他代码 ... |
思维转变: Partial<T>
让我们学会了创建专门用于“更新”场景的类型。我们不再传递完整的数据实体,而是传递一个描述“变化”的、类型安全的 载荷 (Payload)。这是现代状态管理(如 Redux/Pinia)思想的基石。
5.2.2. 核心基石:深入理解 keyof
操作符
痛点: 在我们能随心所欲地“裁剪”类型之前,我们必须先学会如何获取一个类型的所有“钥匙”——也就是它的所有属性名。
解决方案: keyof
操作符正是为此而生。它接收一个对象类型,并返回一个由该对象所有属性名组成的 字符串字面量联合类型。
1 | import { Todo } from './types'; |
keyof
是理解 Pick
和 Omit
的前置钥匙。K extends keyof T
这种泛型约束,正是利用 keyof
来保证我们只能对一个类型已存在的属性进行操作。
5.2.3. Pick<T, K>
与 Omit<T, K>
:类型的“裁剪”
除了 Partial
,还有两个极其常用的映射类型,用于创建现有类型的子集。
Pick<Type, Keys>
: 从Type
中 挑选 出Keys
联合类型中指定的属性。Omit<Type, Keys>
: 从Type
中 忽略 掉Keys
联合类型中指定的属性。
第五步:在我们的 Todo 应用中实践
场景: 假设我们有一个“预览”组件,它只需要显示 Todo 的 text
和 completed
状态,不需要 id
和 dueDate
。
文件路径: src/types.ts
1 | // ... 其他类型 ... |
文件路径: src/main.ts
1 | function renderTodoPreview(preview: TodoPreview): void { |
Pick
和 Omit
是前端开发中处理组件 Props
的利器。它们能帮助我们精确地控制一个组件应该接收哪些数据,不多也不少,从而实现组件之间清晰的边界和依赖关系。
5.3. 条件类型:类型世界的 if-else
如果说映射类型是“批量转换”,那么条件类型就是“逻辑判断”。它允许 TypeScript 根据一个类型是否满足某个条件,来决定最终应用哪个类型。
5.3.1. extends
关键字的妙用
条件类型的语法非常像 JavaScript 的三元运算符:
SomeType extends OtherType ? TrueType : FalseType;
这里的 extends
不再是类的继承,而是类型系统中的一个逻辑判断,意为“SomeType 是否可以赋值给 OtherType”。
场景: 我们需要一个 process
函数,如果传入的是 string
,就返回 string
;如果传入的是 number
,就返回 number
。但如果传入的是 null
或 undefined
,我们希望返回 never
类型,让这种调用在类型层面就变得“不可能”。
1 | // 定义一个条件类型 |
5.4. 实践:掌握核心内置函数工具类型
痛点: 在大型项目中,我们经常需要编写一些工具函数来处理或包装其他函数(例如,添加日志、缓存等)。在 JS 中,我们无法静态地知道被包装函数的参数和返回值类型,只能依赖 any
。
解决方案: TypeScript 提供了 Parameters<T>
和 ReturnType<T>
这两个强大的工具类型,它们都基于 infer
实现,可以精准地“捕获”任何函数的签名。
5.4.1. Parameters<T>
: 获取函数参数类型
Parameters<T>
接收一个函数类型 T
,并返回一个由该函数所有参数类型组成的 元组类型。
1 | function addTodo(text: string, dueDate?: Date): number { |
5.4.2. ReturnType<T>
: 获取函数返回类型
ReturnType<T>
接收一个函数类型 T
,并返回该函数的返回值类型。
1 | // 捕获 addTodo 函数的返回类型 |
5.4.3. 实践:创建一个类型安全的 logFunctionCall
工具
现在,我们将这两个工具类型组合起来,创建一个通用的、完全类型安全的函数日志记录器。
文件路径: src/utils.ts
1 | // F 必须是一个函数类型 |
在 main.ts
中使用它:
1 | import { logFunctionCall } from './utils'; |
5.5 本章小结
高级类型 | 核心价值 | 在我们 Todo 应用中的实践 |
---|---|---|
Partial<T> | 将所有属性变为可选 | (关键) 优雅地实现了 updateTodo 函数,使其能接收“部分更新”的数据。 |
keyof | 获取类型的所有键 | 是理解 Pick /Omit 的基础,用于创建类型安全的泛型约束。 |
Pick /Omit | 从类型中挑选/排除属性 | 创建了一个精确的 TodoPreview 类型,用于只需部分数据的场景。 |
Parameters<T> | 捕获函数参数类型元组 | 结合 ReturnType 创建了类型安全的 logFunctionCall 通用工具函数。 |
ReturnType<T> | 捕获函数返回值类型 | 同上,保证了包装后函数的返回值类型与原函数完全一致。 |