第八节 - 更深入的类型系统:泛型与高级模式
TypeScript 的类型系统远不止于基本类型和固定结构。泛型等高级特性使得我们可以编写更加灵活、可复用、同时又能保证类型安全的代码。这就像在设计智能家居系统时,我们设计了一套“通用接口”,这套接口可以适用于不同类型的设备(灯、风扇、咖啡机),而无需为每一种设备单独设计接口,但在使用时又能明确知道连接的是哪种设备并进行相应操作。
泛型 (<T>
):
泛型允许你编写可以处理多种类型的代码,同时保留类型信息。它们是类型层面的参数化。
泛型函数:
// eighth.ts
// 一个没有泛型的函数,只能处理 string 数组
function getFirstString(arr: string[]): string | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
// 如果需要获取 number 数组的第一个元素,需要重新写一个函数
// function getFirstNumber(arr: number[]): number | undefined { ... }
// 使用泛型,创建一个可以处理任意类型数组的函数
function getFirstElement<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
// 调用泛型函数时,TypeScript 会根据传入的参数自动推断 T 的类型
const firstNum = getFirstElement([1, 2, 3]); // T 被推断为 number
const firstStr = getFirstElement(["a", "b", "c"]); // T 被推断为 string
const firstBool = getFirstElement([true, false]); // T 被推断为 boolean
console.log("第一个数字:", firstNum);
console.log("第一个字符串:", firstStr);
console.log("第一个布尔值:", firstBool);
// 你也可以显式指定泛型类型 (通常不必要,除非 TypeScript 无法正确推断)
const firstElementExplicit = getFirstElement<number>([10, 20]);
泛型接口:
接口也可以使用泛型,来描述包含某种通用类型数据的结构。
// eighth.ts (接着上面的代码)
// 定义一个泛型接口,表示一个包含数据的响应对象
interface ApiResponse<DataType> {
code: number;
message: string;
data: DataType; // data 属性的类型是泛型参数 DataType
}
// 使用泛型接口
// 响应数据是用户列表
interface User { id: number; name: string; }
const userListResponse: ApiResponse<User[]> = {
code: 200,
message: "成功",
data: [{ id: 1, name: "张三" }, { id: 2, name: "李四" }]
};
// 响应数据是单个产品信息
interface Product { id: number; productName: string; price: number; }
const productResponse: ApiResponse<Product> = {
code: 200,
message: "成功",
data: { id: 101, productName: "笔记本电脑", price: 5000 }
};
// 响应数据为空 (使用 null 或 undefined 或特定的空数据类型)
const emptyResponse: ApiResponse<null> = {
code: 204,
message: "无内容",
data: null
};
console.log("用户列表响应:", userListResponse);
console.log("产品响应:", productResponse);
泛型类:
类也可以使用泛型,来创建可以操作泛型数据的组件。
// eighth.ts (接着上面的代码)
// 定义一个泛型栈类 (后进先出)
class Stack<T> {
private elements: T[] = [];
push(element: T): void {
this.elements.push(element);
}
pop(): T | undefined {
return this.elements.pop();
}
isEmpty(): boolean {
return this.elements.length === 0;
}
}
// 创建一个存储数字的栈
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log("数字栈弹出:", numberStack.pop()); // 返回 number | undefined
// 创建一个存储字符串的栈
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log("字符串栈弹出:", stringStack.pop()); // 返回 string | undefined
// 尝试向数字栈推入字符串会报错
// numberStack.push("three"); // 编译时报错
泛型约束 (extends
):
有时候,你希望泛型参数 T 必须满足某个条件(比如必须包含某个属性,或者必须是某个接口的实现)。可以使用 extends
关键字添加泛型约束。这就像要求连接到通用接口的设备必须具备某些基本功能。
// eighth.ts (接着上面的代码)
interface Lengthwise {
length: number; // 要求类型 T 必须有 length 属性
}
// 函数泛型约束
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 现在可以安全地访问 .length 属性
return arg;
}
// 调用时必须传入具有 length 属性的类型
loggingIdentity("hello"); // string 有 length 属性
loggingIdentity([1, 2, 3]); // array 有 length 属性
// loggingIdentity(10); // 编译时报错:Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// loggingIdentity({ name: "test" }); // 编译时报错:Argument of type '{ name: string; }' is not assignable to parameter of type 'Lengthwise'.
Utility Types (内置工具类型):
TypeScript 提供了一些非常有用的内置工具类型,它们可以基于现有类型创建新类型,非常方便。这就像智能家居系统提供了一些现成的“转换器”或“适配器”。
Partial<T>
: 将类型 T 的所有属性变为可选。Readonly<T>
: 将类型 T 的所有属性变为只读。Pick<T, K>
: 从类型 T 中选取属性 K 创建新类型。Omit<T, K>
: 从类型 T 中省略属性 K 创建新类型。Record<K, T>
: 创建一个对象类型,其键是 K 类型,值是 T 类型。
// eighth.ts (接着上面的代码)
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: Date;
}
// Partial: 所有属性可选
type PartialTodo = Partial<Todo>;
/* 等价于:
type PartialTodo = {
title?: string;
description?: string;
completed?: boolean;
createdAt?: Date;
};
*/
const partialTask: PartialTodo = {
title: "学习 TS"
// 其他属性可选
};
// Readonly: 所有属性只读
type ReadonlyTodo = Readonly<Todo>;
/* 等价于:
type ReadonlyTodo = {
readonly title: string;
readonly description: string;
readonly completed: boolean;
readonly createdAt: Date;
};
*/
const readonlyTask: ReadonlyTodo = {
title: "完成报告",
description: "提交周报",
completed: false,
createdAt: new Date()
};
// readonlyTask.completed = true; // 编译时报错:Cannot assign to 'completed' because it is a read-only property.
// Pick: 选取部分属性
type TodoPreview = Pick<Todo, "title" | "completed">;
/* 等价于:
type TodoPreview = {
title: string;
completed: boolean;
};
*/
const taskPreview: TodoPreview = {
title: "购买食材",
completed: false
};
// Omit: 省略部分属性
type TodoWithoutDates = Omit<Todo, "createdAt">;
/* 等价于:
type TodoWithoutDates = {
title: string;
description: string;
completed: boolean;
};
*/
const taskWithoutDate: TodoWithoutDates = {
title: "打扫房间",
description: "彻底清洁",
completed: true
};
// Record: 创建键值对类型
type Status = "open" | "closed";
type Issue = { title: string; description: string };
type IssueTracker = Record<Status, Issue[]>;
/* 等价于:
type IssueTracker = {
open: Issue[];
closed: Issue[];
};
*/
const issueData: IssueTracker = {
open: [{ title: "Bug 1", description: "..."}, { title: "Feature 2", description: "..."}],
closed: [{ title: "Task 3", description: "..."}]
};
console.log("部分任务:", partialTask);
console.log("只读任务:", readonlyTask);
console.log("任务预览:", taskPreview);
console.log("不含日期任务:", taskWithoutDate);
console.log("问题追踪器:", issueData);
条件类型 (Conditional Types):
条件类型允许你根据一个类型是否符合某个约束,来决定最终的类型。语法是 T extends U ? X : Y
。如果类型 T
可以赋值给类型 U
,则结果是 X
类型,否则是 Y
类型。这就像智能家居系统中的一个逻辑判断模块:如果设备是灯,就执行照明操作;如果是风扇,就执行通风操作。
// eighth.ts (接着上面的代码)
// 如果 T 是 string 类型,则结果是 string[],否则是 T
type StringArrayOrT<T> = T extends string ? string[] : T;
let result1: StringArrayOrT<string>; // result1 的类型是 string[]
result1 = ["hello", "world"];
let result2: StringArrayOrT<number>; // result2 的类型是 number
result2 = 123;
let result3: StringArrayOrT<boolean>; // result3 的类型是 boolean
result3 = true;
// 一个更实用的例子:提取函数的返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// 如果 T 是一个函数类型 (...args: any[] 代表任意参数的函数),则提取其返回值类型 R,否则是 any
// infer R 是一个关键字,用于在 extends 条件中推断类型变量
function getUser(): User { // User 接口在上面定义过
return { id: 1, name: "Alice" };
}
function sayHello(): void {
console.log("hello");
}
type GetUserReturnType = ReturnType<typeof getUser>; // GetUserReturnType 的类型是 User
const userResult: GetUserReturnType = getUser();
console.log("GetUser 返回类型示例:", userResult);
type SayHelloReturnType = ReturnType<typeof sayHello>; // SayHelloReturnType 的类型是 void
const sayHelloResult: SayHelloReturnType = sayHello();
console.log("SayHello 返回类型示例:", sayHelloResult);
映射类型 (Mapped Types):
映射类型允许你遍历一个类型的属性,并为每个属性应用一个转换。这就像智能家居系统中的一个配置面板,可以批量修改所有设备的某个属性(比如将所有灯的亮度增加 10%)。
// eighth.ts (接着上面的代码)
type Properties = 'prop1' | 'prop2' | 'prop3';
// 将 Properties 中的每个属性都转换为 boolean 类型
type MyMappedType = {
[P in Properties]: boolean;
// [P in keyof SomeType]: AnotherType; // 更常见的用法是遍历现有类型的属性
};
/* 等价于:
type MyMappedType = {
prop1: boolean;
prop2: boolean;
prop3: boolean;
};
*/
const myFlags: MyMappedType = {
prop1: true,
prop2: false,
prop3: true
};
console.log("映射类型示例:", myFlags);
// 一个常见的映射类型:ReadOnly
type ReadonlyMapped<T> = {
readonly [P in keyof T]: T[P];
};
interface Point {
x: number;
y: number;
}
type ReadonlyPoint = ReadonlyMapped<Point>;
/* 等价于:
type ReadonlyPoint = {
readonly x: number;
readonly y: number;
};
*/
const p: ReadonlyPoint = { x: 10, y: 20 };
// p.x = 5; // 编译时报错:Cannot assign to 'x' because it is a read-only property.
编译 eighth.ts
:
tsc eighth.ts
会生成 eighth.js
文件。然后在 HTML 中引入 eighth.js
。
小结: 泛型是构建灵活、可复用组件的关键,它使得类型系统能够适应多种数据类型。Utility Types、条件类型和映射类型等高级特性则进一步增强了 TypeScript 类型系统的表达能力,让你能够以声明式的方式操作和转换类型。掌握这些特性可以让你编写出更强大、更安全的 TypeScript 代码。
练习:
- 编写一个泛型函数
reverseArray<T>(arr: T[]): T[]
,它接收一个任意类型的数组,并返回一个新数组,其中元素的顺序是反转的。 - 定义一个泛型接口
KeyValuePair<K, V>
,表示一个键值对,键的类型是 K,值的类型是 V。 - 使用
Partial
Utility Type 创建一个新类型PartialUser
,它是基于你之前定义的User
接口,但所有属性都是可选的。创建一个PartialUser
对象。 - (进阶)尝试编写一个条件类型
ExcludeNullOrUndefined<T>
,如果类型T
是null
或undefined
,则排除它,否则保留T
。例如ExcludeNullOrUndefined<string | number | null | undefined>
的结果应该是string | number
。 - (进阶)尝试编写一个映射类型
ToBoolean<T>
,它将类型T
的所有属性值类型都转换为boolean
类型。
至此,我们已经覆盖了 TypeScript 的大部分核心概念和一些高级特性。从最初的类型注解,到描述复杂数据结构的接口和类,再到灵活强大的泛型和高级类型工具,TypeScript 为 JavaScript 世界带来了前所未有的健壮性和可维护性。掌握了这些知识,你就能更好地构建和管理复杂的 Web 应用,让你的代码“灵魂”不仅活跃,而且坚不可摧!
当然,TypeScript 还有更多细节和高级用法,比如装饰器 (Decorators)、Mixins、声明合并 (Declaration Merging) 等。但以上这些内容已经为你打下了坚实的基础,足以应对绝大多数的开发场景。
下一步:
- 在实际项目中应用 TypeScript,逐步将你的 JavaScript 代码迁移到 TypeScript。
- 阅读 TypeScript 官方文档,深入了解更多细节和最新特性。
- 探索与各种框架和库(如 React, Vue, Angular)集成 TypeScript 的最佳实践。
- 学习如何编写
.d.ts
声明文件,为你自己或社区提供类型信息。
祝你在 TypeScript 的学习和实践之路上一切顺利!