日常代码中的 TypeScript 泛型

Frontend2026-10-05TryQuickToolBox

你正在编写一个返回数组第一个元素的函数。你希望它能处理数字、字符串和对象,但你也希望返回类型与输入类型匹配。没有泛型,你只能求助于 any 并失去所有类型安全。这就是 TypeScript 泛型的用武之地。

泛型让你编写能处理多种类型的代码,同时保留具体的类型信息。在本指南中,我们将探讨在日常 TypeScript 代码中使用泛型的实用模式。

什么是泛型?

泛型是一种参数化类型的方式。就像函数接受参数一样,泛型接受类型参数。它们允许你创建可复用的组件,这些组件可以处理多种类型,而不是单一类型。

考虑这个恒等函数:

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

这里,T 是一个类型变量。当你调用 identity(42) 时,TypeScript 推断 T 为 number。当你调用 identity("hello") 时,T 变为 string。返回类型始终与输入类型匹配。

实践中的泛型函数

让我们看一个常见场景:一个将值包装在对象中的函数。

function wrapInObject<T>(value: T): { value: T } {
  return { value };
}

没有泛型,你不得不使用 any 或 unknown,调用者会丢失类型信息。有了泛型,返回类型精确为 { value: T }。

另一个日常示例是返回数组最后一个元素的函数:

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

TypeScript 从数组参数推断 T,所以 last([1, 2, 3]) 返回 number | undefined。

泛型约束

有时你想限制可用于泛型的类型。例如,你可能需要访问仅存在于某些类型上的属性。你可以使用 extends 关键字添加约束。

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

现在 T 必须有一个类型为 number 的 length 属性。这适用于字符串、数组以及任何具有 length 属性的对象。

你也可以对接口使用约束:

interface HasId {
  id: string;
}

function findById<T extends HasId>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}

这确保传递给 findById 的项具有 id 属性。

泛型接口和类型别名

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

interface ApiResponse<T> {
  data: T;
  status: number;
  error?: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return { data, status: response.status };
}

现在你可以精确地类型化 API 响应:

interface User {
  id: number;
  name: string;
}

const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User

泛型类

类也可以是泛型的。一个常见示例是简单的栈:

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

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

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

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined

React 组件中的泛型

如果你正在构建 React 组件,泛型可以让你的组件更可复用。例如,一个渲染任意类型项的列表组件:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}

现在你可以将 List 与任何类型一起使用,并在 renderItem 回调上获得完整的类型检查。

常见陷阱和最佳实践

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

方法 类型安全 灵活性 使用场景
any 无 高 迁移 JS、快速原型
联合类型 好 限于已知类型 当你知道所有可能的类型时
泛型 强 高 保留类型的可复用组件

常见问题

什么时候应该使用泛型而不是 any?

每当你想在函数或组件中保留类型信息时,就使用泛型。any 禁用类型检查,而泛型保持它。如果你发现自己为了让它工作而使用 any,考虑一下泛型是否能更安全地解决它。

我可以使用多个类型参数吗?

是的。你可以定义带有多个类型参数的函数,例如 function pair<K, V>(key: K, value: V): [K, V]。当有多个参数时,使用描述性名称。

泛型在 .tsx 文件中如何与箭头函数一起工作?

在 .tsx 文件中,<T> 可能与 JSX 产生歧义。使用尾随逗号:const identity = <T,>(arg: T): T => arg; 或使用 extends unknown:<T extends unknown>。

泛型是一个强大的功能,可以让你的 TypeScript 代码更可复用和类型安全。从简单的泛型函数开始,然后逐渐将它们应用到接口和类。如果你需要格式化或验证 JSON 数据并保证类型安全,请查看我们的 JSON Formatter 工具来快速检查和美化打印你的 JSON。