泛型类与泛型接口
本教程共 80 篇 · 第 40 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:掌握泛型类的完整设计、泛型接口作为”契约”的模式、非泛型类上的泛型方法,以及泛型在类继承链中的传递。
上一章我们给函数加约束,这一章我们把泛型放到更大的结构里——类和接口。这是 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 内置的工具类型——你会发现很多工具类型的本质就是泛型。