返回文章列表
技术2026-05-28

TypeScript 泛型完全指南

从泛型函数、泛型接口到条件类型和工具类型,全面掌握 TypeScript 泛型的核心概念与实战技巧。

为什么需要泛型

假设你要写一个函数,返回数组的第一个元素。如果没有泛型,你可能会这样写:

function first(arr: any[]): any {
  return arr[0];
}

const num = first([1, 2, 3]);  // 类型是 any,丢失了类型信息
const str = first(['a', 'b']); // 同样是 any

这段代码的问题在于,返回值的类型信息完全丢失了。调用者不知道 numnumber 还是 string。泛型就是为解决这类问题而生的——它让你在定义函数、接口或类时,将类型作为参数传入,从而在保持类型安全的同时实现复用。

泛型函数

泛型函数的基本语法是在函数名后面加上尖括号 <T>

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

const num = first([1, 2, 3]);   // number | undefined
const str = first(['a', 'b']);  // string | undefined

TypeScript 会自动推断 T 的类型,大多数情况下你不需要手动指定。但在一些复杂场景下,显式标注能让代码意图更清晰:

const result = first<number>([1, 2, 3]);

泛型接口与泛型类型别名

泛型不仅可以用在函数上,还可以用在接口和类型别名中:

// 泛型接口
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

// 泛型类型别名
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

使用泛型接口的经典场景是封装 API 请求:

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

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

泛型约束(extends)

有时候你希望泛型参数满足某些条件。比如,你写了一个函数,需要访问参数的 length 属性,但 T 可能是任何类型,不一定有 length。这时你可以用 extends 关键字添加约束:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): T {
  console.log(item.length);
  return item;
}

logLength('hello');     // OK, string 有 length
logLength([1, 2, 3]);   // OK, array 有 length
// logLength(123);       // Error! number 没有 length

约束的另一个常见用途是确保泛型参数是某个对象的键:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
getProperty(user, 'name');  // OK, 返回 string
// getProperty(user, 'age'); // Error!

keyof 操作符

keyof 是泛型编程中极为重要的操作符。它获取一个类型的所有键,返回一个联合类型:

interface Person {
  name: string;
  age: number;
  location: string;
}

type PersonKeys = keyof Person; // 'name' | 'age' | 'location'

keyof 经常和 extends 结合使用,用来约束泛型参数必须是对象的键。此外,它还是很多工具类型的基础。

条件类型(extends + infer)

条件类型是 TypeScript 类型系统中最强大的特性之一。它的语法类似三元表达式:

type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<string>;  // 'yes'
type B = IsString<number>;  // 'no'

配合 infer 关键字,你可以在条件类型中"提取"部分类型信息:

// 提取数组元素类型
type ArrayItem<T> = T extends (infer U)[] ? U : never;

type Item1 = ArrayItem<string[]>;          // string
type Item2 = ArrayItem<number[]>;           // number

// 提取函数返回值类型
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type FnReturn = ReturnOf<Fn>;               // string

常用工具类型

TypeScript 内置了一系列实用工具类型,它们本质上都是基于泛型和条件类型实现的。掌握它们能显著提升你的类型体操水平:

Partial<T> —— 全部变可选

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

function updateTodo(id: number, fields: Partial<Todo>) {
  // fields 的每个属性都是可选的
}

updateTodo(1, { completed: true }); // OK

Pick<T, K> —— 选取指定属性

type TodoPreview = Pick<Todo, 'title' | 'completed'>;
// { title: string; completed: boolean; }

Omit<T, K> —— 排除指定属性

type TodoWithoutDesc = Omit<Todo, 'description'>;
// { title: string; completed: boolean; }

Record<K, V> —— 构建键值对类型

type PageInfo = Record<'home' | 'about' | 'contact', { title: string; url: string }>;
// { home: { ... }; about: { ... }; contact: { ... }; }

其它常用工具类型速查

泛型 React 组件实战

在 React 中,泛型最常见的应用场景是写可复用的列表、表单和选择器组件。以下是一个泛型 Select 组件的例子:

interface SelectProps<T> {
  options: T[];
  value: T;
  onChange: (value: T) => void;
  getLabel: (option: T) => string;
  getValue: (option: T) => string;
}

function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select
      value={getValue(value)}
      onChange={(e) => {
        const selected = options.find(
          (opt) => getValue(opt) === e.target.value
        );
        if (selected) onChange(selected);
      }}
    >
      {options.map((opt) => (
        <option key={getValue(opt)} value={getValue(opt)}>
          {getLabel(opt)}
        </option>
      ))}
    </select>
  );
}

这个组件可以接受任意类型的选项——不管是简单的字符串数组,还是复杂的对象数组,都能提供完整的类型推导。

泛型的三个实用模式

模式一:泛型工厂函数

当你需要创建一系列结构相同但类型不同的对象时,泛型工厂函数非常有用:

function createList<T>(items: T[]): { items: T[]; add(item: T): void; remove(index: number): void } {
  return {
    items,
    add(item: T) { this.items.push(item); },
    remove(index: number) { this.items.splice(index, 1); },
  };
}

const numberList = createList<number>([1, 2, 3]);
const stringList = createList<string>(['a', 'b']);

模式二:类型安全的 Event Emitter

type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string; ctrl: boolean };
  focus: undefined;
};

class TypedEmitter<T extends Record<string, any>> {
  private handlers = new Map<keyof T, Set<Function>>();

  on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    this.handlers.get(event)?.forEach((fn) => fn(data));
  }
}

const emitter = new TypedEmitter<EventMap>();
emitter.on('click', ({ x, y }) => console.log(x, y)); // 完全类型安全

模式三:Builder 模式

class QueryBuilder<T> {
  private conditions: string[] = [];

  where<K extends keyof T>(field: K, value: T[K]): this {
    this.conditions.push(`${String(field)} = ${JSON.stringify(value)}`);
    return this;
  }

  build(): string {
    return `SELECT * WHERE ${this.conditions.join(' AND ')}`;
  }
}

interface Product { id: number; name: string; price: number; }
const sql = new QueryBuilder<Product>()
  .where('price', 99)
  .where('name', 'Widget')
  .build();

总结

泛型是 TypeScript 类型系统的基石。从简单的泛型函数开始,到泛型接口、泛型约束,再到条件类型和工具类型——每一步都在让你的代码变得更加类型安全的同时,保持高度的可复用性。建议从日常开发中最常见的场景开始练习:用 Partial 做更新接口、用 Pick/Omit 做 DTO 类型、用泛型封装 API 请求。当这些变成肌肉记忆之后,再逐步深入到条件类型和 infer 的高级用法。


返回文章列表
标签:TypeScript泛型前端开发