Appearance
title: TypeScript 入门到实战(四):代码复用与抽象 —— 泛型 (Generics) 的魔力 date: 2025-07-01 tags: [TypeScript, 泛型, Generics, 类型抽象, 代码复用] category: 前端开发 description: TypeScript 从入门到实战系列第4篇:深入学习泛型的概念和应用,掌握如何编写高度灵活、可复用且类型安全的代码组件 series: TypeScript从入门到实战 seriesOrder: 4
TypeScript 入门到实战(四):代码复用与抽象 —— 泛型 (Generics) 的魔力
摘要: 本文将介绍 TypeScript 中极具魔力的特性——泛型(Generics)。文章从一个简单的问题(如何在保持类型信息的同时创建可复用函数)入手,循序渐进地讲解了泛型函数、泛型接口和泛型约束的语法与应用场景。通过学习本篇,读者将能编写出高度灵活、可复用且类型安全的代码组件,真正发挥 TypeScript 的强大威力。
关键词: TypeScript 泛型, TypeScript Generics, 泛型函数, 泛型接口, 泛型约束, T, 代码复用, 类型抽象
在上一篇文章中,我们学会了使用 interface
和 type
来为各种数据结构建立精确的“蓝图”。现在,我们可以自信地定义一个 User
对象或一个 Product
对象。
但新的挑战随之而来:如果我们想编写一个通用的函数,它既能服务于 User
数组,也能服务于 Product
数组,同时还能保持各自严格的类型信息,该怎么做?
这就是今天要探讨的主题——泛型(Generics)。它是 TypeScript 中实现代码复用与类型安全完美结合的核心工具。
1. 问题的提出:一个“健忘”的函数
假设我们需要一个函数,它的功能很简单:接收一个数组,并返回该数组的第一项。
第一版尝试:使用 any
一个不了解泛型的开发者可能会这样写:
typescript
function getFirstElement(arr: any[]): any {
return arr[0];
}
const names = ["Alice", "Bob", "Eve"];
const firstNumber = getFirstElement([1, 2, 3]); // -> 1
const firstString = getFirstElement(names); // -> "Alice"
// 问题来了:
firstNumber.toFixed(2); // 在编码时,这里没有任何错误提示!
// 但 firstNumber 是数字,这行代码在运行时是OK的。
firstString.toFixed(2); // 编码时也OK,但运行时会报错,因为字符串没有 toFixed 方法!
any
方案虽然实现了函数的复用,但完全牺牲了类型安全。TypeScript 对 firstNumber
和 firstString
的类型一无所知,这让我们又回到了纯 JavaScript 的“危险”境地。
2. 泛型的魔力:类型变量 <T>
泛型使用一个类型变量(通常用 <T>
表示)来作为类型的“占位符”。当函数被调用时,TypeScript 会根据传入的参数自动推断出这个占位符应该代表的具体类型。
让我们用泛型重写上面的函数:
typescript
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
让我们来解析一下这个语法:
<T>
:在函数名后声明一个类型变量T
。T
在这里代表一个未知的类型。arr: T[]
: 参数arr
的类型是“由 T 组成的数组”。: T | undefined
: 函数的返回值类型是T
(如果数组不为空)或undefined
(如果数组为空)。
现在再来看看它的表现:
typescript
// 传入数字数组
const firstNum = getFirstElement([1, 2, 3]);
// TS 自动推断 T 为 number,所以 firstNum 的类型是 number | undefined
// firstNum.toUpperCase(); // 错误:类型“number”上不存在属性“toUpperCase”
// 传入字符串数组
const firstStr = getFirstElement(["Alice", "Bob"]);
// TS 自动推断 T 为 string,所以 firstStr 的类型是 string | undefined
// firstStr.toFixed(2); // 错误:类型“string”上不存在属性“toFixed”
看!我们得到了一个既可复用又完全类型安全的函数。TypeScript 像一个聪明的侦探,根据我们传入的实际参数,准确地推断出了 T
应该是什么,并在整个函数调用链中保持了这个类型信息。
3. 泛型的应用场景
泛型的威力远不止于此,它可以应用于函数、接口、类等多种场景。
泛型接口
假设我们后端 API 的返回数据结构总是遵循一个固定模式。我们可以用泛型接口来定义它:
typescript
interface ApiResponse<T> {
success: boolean;
data: T;
code: number;
message?: string;
}
// 假设我们有 User 和 Product 两个接口
interface User { name: string; age: number; }
interface Product { name: string; price: number; }
// 现在可以精确地定义不同 API 的返回类型
const userResponse: ApiResponse<User> = {
success: true,
data: { name: "Alice", age: 30 },
code: 200,
};
const productResponse: ApiResponse<Product[]> = {
success: true,
data: [
{ name: "Laptop", price: 8000 },
{ name: "Mouse", price: 200 },
],
code: 200,
};
这样,我们在处理 userResponse.data
时,就能获得完整的 User
类型提示;处理 productResponse.data
时,就能获得 Product
数组的类型提示。
泛型类
泛型在类中的应用也非常普遍,例如创建一个通用的数据结构:
typescript
class DataStore<T> {
private data: T[] = [];
addItem(item: T): void {
this.data.push(item);
}
getItems(): T[] {
return [...this.data];
}
}
// 创建一个只存储字符串的实例
const stringStore = new DataStore<string>();
stringStore.addItem("Hello");
stringStore.addItem("World");
// stringStore.addItem(123); // 错误:参数类型 number 不能赋值给 string
// 创建一个只存储用户的实例
const userStore = new DataStore<User>();
userStore.addItem({ name: "Bob", age: 40 });
4. 为泛型添加“约束”
有时候,我们希望我们的泛型函数只能处理某些特定结构的类型。例如,一个函数需要打印参数的 .length
属性。
typescript
// 这个会报错,因为 TS 不确定 T 是否一定有 length 属性
// function printLength<T>(arg: T): void {
// console.log(arg.length); // 错误: 类型“T”上不存在属性“length”。
// }
为了解决这个问题,我们可以使用泛型约束,通过 extends
关键字来限制 T
的范围。
typescript
interface WithLength {
length: number;
}
function printLength<T extends WithLength>(arg: T): void {
console.log(arg.length);
}
printLength("Hello World"); // OK, string 有 length 属性
printLength([1, 2, 3]); // OK, array 有 length 属性
printLength({ length: 10, value: '...' }); // OK, 对象有 length 属性
// printLength(123); // 错误: 类型“number”的参数不能赋给类型“WithLength”的参数。
T extends WithLength
的意思是:“T
可以是任何类型,但它必须符合 WithLength
接口的结构(即至少要有一个 number
类型的 length
属性)。”
5. 真实世界中的泛型
你可能已经在不知不觉中每天都在使用泛型了。
Promise<T>:
fetch
函数返回一个Promise
对象。这个Promise
解析后的数据类型是什么?这正是泛型的用武之地。Promise<Response>
表示这个Promise
会解析为一个Response
对象。React
useState<T>
: 在 React 中,useState
Hook 是一个典型的泛型应用。typescriptconst [user, setUser] = useState<User | null>(null); // TS 推断出: // user 的类型是 User | null // setUser 的类型是 React.Dispatch<React.SetStateAction<User | null>> // 这意味着你只能用 User 或 null 类型的值去调用 setUser
总结与展望
今天,我们揭开了泛型 <T>
的神秘面纱。我们了解到,泛型是 TypeScript 类型系统的“灵魂”之一,它使得我们在不牺牲类型安全的前提下,编写出最大程度可复用的代码成为可能。我们学习了如何定义泛型函数、接口、类,并使用 extends
关键字为泛型添加约束。
至此,你已经掌握了 TypeScript 中从基础到抽象的核心工具。
接下来,我们将更上一层楼。既然我们能定义各种类型,那能不能对“类型”本身进行编程和转换呢?比如,如何将一个接口的所有属性都变成可选的?TypeScript 提供了一系列内置的“类型工具”,能极大地提升我们的开发效率。
在下一篇文章**《TypeScript 入门到实战(五):高级类型进阶 - 掌握 Utility Types、条件类型与 keyof》**中,我们将一起探索这些强大的“类型魔法”。