日常代码中的 TypeScript 泛型:实用指南

Frontend2026-09-21TryQuickToolBox

你正在编写一个可以处理任何类型的函数,但不想失去类型安全性。你可以使用 any,但这违背了 TypeScript 的初衷。泛型允许你编写可处理多种类型的代码,同时保留类型信息。本指南将向你展示如何在日常代码中使用泛型,而不会把事情搞得太复杂。

什么是泛型?

泛型是一种创建可重用组件的方式,这些组件可以处理多种类型,而不是单一类型。它们允许你捕获输入的类型,并在输出中使用它,从而在不牺牲灵活性的情况下确保类型安全。

把泛型想象成类型参数。就像函数接受值参数一样,泛型函数接受类型参数。例如,恒等函数:

function identity<T>(arg: T): T {
  return arg;
}

这里,T 是一个类型参数。当你调用 identity<string>("hello") 时,TypeScript 推断出 T 是 string,返回类型也是 string。如果你传入一个数字,它返回一个数字。这比使用 any 更精确。

为什么不直接使用 any?

使用 any 会禁用类型检查。你会失去自动补全、重构支持,编译器也无法捕获错误。泛型让类型信息在代码中流动,因此你既能获得灵活性,又能保证安全。

实践中的泛型函数

让我们看一个常见场景:一个返回数组第一个元素的函数。

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

没有泛型时,你可能会写 function firstElement(arr: any[]): any,这会丢失类型。使用泛型,如果你传入一个 string[],返回类型就是 string | undefined。

你也可以使用多个类型参数。例如,一个使用回调将数组映射为新数组的函数:

function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

这本质上就是 TypeScript 标准库中 Array.prototype.map 的类型定义方式。

泛型接口和类型别名

泛型不仅限于函数。你可以创建泛型接口和类型别名来定义可重用的数据结构。

interface Box<T> {
  value: T;
}

const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };

这对于 API 响应、状态管理或任何容器类型都很有用。

你还可以提供默认类型:

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

现在,不提供类型参数的 ApiResponse 默认为 unknown。

泛型约束

有时你想限制泛型可以使用的类型。你可以使用 extends 关键字来添加约束。

function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

现在 T 必须具有 length 属性。你可以调用 getLength("hello") 或 getLength([1,2,3]),但不能调用 getLength(42)。

当处理共享共同形状的对象时,约束非常强大。

在类中使用泛型

类也可以是泛型的。一个常见的例子是泛型集合:

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const popped = stack.pop(); // number | undefined

这确保了只能将数字推入数字栈。

实际示例:类型安全的事件发射器

让我们构建一个简单的事件发射器,它使用泛型将事件名称映射到负载类型。

type EventMap = Record<string, any>;

class EventEmitter<T extends EventMap> {
  private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    this.listeners[event]?.forEach(listener => listener(payload));
  }
}

// Usage
interface MyEvents {
  login: { userId: string };
  logout: { userId: string };
}

const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (payload) => {
  console.log(payload.userId); // string
});
emitter.emit('login', { userId: '123' }); // OK
// emitter.emit('login', { userId: 123 }); // Error

这种模式为事件名称和负载提供了完整的类型安全,减少了运行时错误。

常见陷阱和最佳实践

比较:泛型 vs. any vs. 联合类型

方法 类型安全 灵活性 用例
泛型 高 高 保留类型的可重用组件
any 无 高 快速原型设计、迁移 JS
联合类型 中 中 已知类型的有限集合

泛型在灵活性和安全性之间取得了平衡,使其成为库和共享工具的理想选择。

常见问题

什么时候应该使用泛型而不是联合类型?

当输入和输出之间的类型关系很重要时,使用泛型。联合类型适用于值可以是固定类型集合中的一种,但具体类型不需要在操作中保留的情况。

我可以在箭头函数中使用泛型吗?

可以,但在 .tsx 文件中语法可能比较棘手。你可以编写 const identity = <T,>(arg: T): T => arg;,并加上尾随逗号以避免 JSX 歧义。

如何将泛型约束为仅接受对象?

使用 T extends object 或 T extends Record<string, unknown> 来确保 T 是对象类型。

泛型是 TypeScript 类型系统的基石。通过在日常代码中使用它们,你可以编写既灵活又安全的函数、接口和类。从简单的泛型函数开始,然后根据需要逐渐引入约束和更复杂的模式。如需更多开发者工具,请查看我们的 JSON Formatter,快速验证和格式化你的 JSON 数据。