Skip to content

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, 代码复用, 类型抽象


在上一篇文章中,我们学会了使用 interfacetype 来为各种数据结构建立精确的“蓝图”。现在,我们可以自信地定义一个 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 对 firstNumberfirstString 的类型一无所知,这让我们又回到了纯 JavaScript 的“危险”境地。

2. 泛型的魔力:类型变量 <T>

泛型使用一个类型变量(通常用 <T> 表示)来作为类型的“占位符”。当函数被调用时,TypeScript 会根据传入的参数自动推断出这个占位符应该代表的具体类型。

让我们用泛型重写上面的函数:

typescript
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

让我们来解析一下这个语法:

  • <T>:在函数名后声明一个类型变量 TT 在这里代表一个未知的类型。
  • 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 是一个典型的泛型应用。

    typescript
    const [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》**中,我们将一起探索这些强大的“类型魔法”。

Last updated: