首页 / TypeScript 入门教程 / 泛型类与泛型接口

TypeScript 入门教程

泛型类与泛型接口

本教程共 80 篇 · 第 40 篇 · 更新于 2026-08-10 · 约 10 分钟阅读

TypeScriptTypeScript 入门教程泛型泛型类泛型接口

本节目标:掌握泛型类的完整设计、泛型接口作为”契约”的模式、非泛型类上的泛型方法,以及泛型在类继承链中的传递。

上一章我们给函数加约束,这一章我们把泛型放到更大的结构里——类和接口。这是 TypeScript 中用来组织代码的两个核心单位,泛型让它们同时保持复用性和类型安全。

泛型类:一个完整的 Stack

第 38 章我们看过一个极简的栈。这里做一个更完整的版本——支持批量初始化、迭代遍历:

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

  constructor(initialItems: T[] = []) {
    this.items = [...initialItems];
  }

  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];
  }

  get size(): number {
    return this.items.length;
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }

  // 实现可迭代,支持 for...of
  *[Symbol.iterator](): IterableIterator<T> {
    for (let i = this.items.length - 1; i >= 0; i--) {
      yield this.items[i];
    }
  }
}

const stack = new Stack([1, 2, 3]);
stack.push(4);

for (const item of stack) {
  console.log(item); // 4, 3, 2, 1(栈顶到栈底)
}

关键在于:所有跟元素类型相关的地方——items 的类型、push 的参数、pop 的返回值、迭代器产出的类型——全部通过 T 关联在一起。换类型只需要改 <T>,不用动任何实现代码。

泛型只作用在实例侧

有一个重要细节:泛型类的静态成员不能使用类的类型参数

class Container<T> {
  private static defaultValue: T; // ❌ 编译错误
  // 静态成员不能引用类型参数 "T"
}

// 正确做法:静态成员用独立的泛型参数
class Container<T> {
  private value: T;

  // 静态方法可以有自己的泛型参数
  static create<U>(value: U): Container<U> {
    return new Container(value);
  }

  private constructor(value: T) {
    this.value = value;
  }
}

原因是静态成员属于类本身,不属于实例。T 只有在创建实例时才被”填入”具体类型,静态侧不知道该绑定哪个 T

泛型接口:定义可复用的契约

接口(interface)跟类一样可以接受泛型参数。常见模式是用泛型接口描述数据仓库:

interface Repository<T> {
  findById(id: string): T | undefined;
  findAll(): T[];
  save(entity: T): void;
  delete(id: string): void;
}

// 用户仓库——绑定 T = User
interface User {
  id: string;
  name: string;
  email: string;
}

class UserRepository implements Repository<User> {
  private users: Map<string, User> = new Map();

  findById(id: string): User | undefined {
    return this.users.get(id);
  }

  findAll(): User[] {
    return Array.from(this.users.values());
  }

  save(user: User): void {
    this.users.set(user.id, user);
  }

  delete(id: string): void {
    this.users.delete(id);
  }
}

Repository<T> 定义了一组标准操作,只要更换 T 就能产出不同类型的仓库。这在分层架构里非常常见——每一层的数据访问对象都实现同一个泛型接口。

泛型接口作为函数类型

接口也可以描述函数:

// 泛型比较器——用来比较两个同类型的值
interface Comparator<T> {
  (a: T, b: T): number;
}

const numberComparator: Comparator<number> = (a, b) => a - b;
const stringComparator: Comparator<string> = (a, b) => a.localeCompare(b);

// 泛型排序函数
function sort<T>(arr: T[], cmp: Comparator<T>): T[] {
  return [...arr].sort(cmp);
}

sort([3, 1, 4, 1, 5], numberComparator); // [1, 1, 3, 4, 5]
sort(["c", "a", "b"], stringComparator);  // ["a", "b", "c"]

非泛型类上的泛型方法

一个类本身不一定是泛型的——但它的某个方法可以是:

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

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

  static swap<T>(arr: T[], i: number, j: number): T[] {
    const result = [...arr];
    [result[i], result[j]] = [result[j], result[i]];
    return result;
  }
}

const firstNum = ArrayUtils.first([1, 2, 3]); // number | undefined
const lastStr = ArrayUtils.last(["a", "b"]);   // string | undefined
const swapped = ArrayUtils.swap([1, 2, 3], 0, 2); // number[]

每个方法有自己的类型参数,互不影响。ArrayUtils 本身不是泛型类,但它的方法各自泛型。

泛型在类继承中的传递

子类可以继承父类的泛型参数,也可以添加自己的:

// 基础集合
class Collection<T> {
  protected items: T[] = [];

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

  getAll(): T[] {
    return [...this.items];
  }
}

// 只读集合——泛型参数传递给父类,自己不加新参数
class ReadonlyCollection<T> extends Collection<T> {
  // 覆写掉修改方法
  override add(item: T): void {
    throw new Error("只读集合,不能添加元素");
  }
}

// 索引集合——父类泛型参数传递 + 自己新增一个 K 参数
class IndexedCollection<T, K extends string | number> extends Collection<T> {
  private index: Map<K, T[]> = new Map();

  addToIndex(key: K, item: T): void {
    this.add(item);
    const existing = this.index.get(key) ?? [];
    existing.push(item);
    this.index.set(key, existing);
  }

  getByKey(key: K): T[] {
    return this.index.get(key) ?? [];
  }
}

泛型参数的传递是”从上到下”的——子类声明时决定父类的 T 是什么,可以传递自己的 T(保持灵活性),也可以写死一个具体类型(锁定父类行为)。

场景:写死父类泛型

有时你想把父类锁定到某个具体类型,同时子类保持自己的灵活性:

// 父类固定处理 string
class StringCollection extends Collection<string> {
  longest(): string {
    return this.items.reduce((a, b) => a.length > b.length ? a : b);
  }
}

const col = new StringCollection();
col.add("hello");
col.add("world!");
console.log(col.longest()); // "world!"

工厂模式:用泛型创建实例

一个常见的泛型模式——构造函数签名约束:

// { new(): T } 表示"一个可以 new 出 T 的构造函数"
function createInstance<T>(ctor: new () => T): T {
  return new ctor();
}

class Dog {
  bark() { console.log("汪汪!"); }
}

class Cat {
  meow() { console.log("喵~"); }
}

const dog = createInstance(Dog);
dog.bark(); // "汪汪!"

const cat = createInstance(Cat);
cat.meow(); // "喵~"

更进一步,构造函数可以带参数:

// 带参数的工厂
function create<T, A extends any[]>(
  ctor: new (...args: A) => T,
  ...args: A
): T {
  return new ctor(...args);
}

class Person {
  constructor(public name: string, public age: number) {}
}

const p = create(Person, "张三", 25);
console.log(p.name, p.age); // "张三" 25

小结

泛型类与泛型接口是 TypeScript 中组织可复用代码的两大支柱:

  • 泛型类 让数据结构(栈、队列、集合)同时服务多种元素类型。
  • 泛型接口 定义契约,让不同实现共享同一个类型安全的签名。
  • 泛型方法 让非泛型类也能在方法级别获得灵活性。
  • 继承体系 中泛型参数可以传递、可以新增、可以锁定。

一个实用的直觉:凡是看到”同一种操作对不同类型做同样的事”,就该考虑泛型类或泛型接口。 下一章我们看看 TypeScript 内置的工具类型——你会发现很多工具类型的本质就是泛型。