TypeScript 泛型接口与泛型类 — 类型工厂的工程化形态
函数层面的泛型你已经会了。今天把
<T>装进 interface 和 class——这是泛型从"工具函数"升级为"架构模式"的一天:Page<T> 统一所有分页接口、Repository<T> 一套增删改查服务全部实体、Result<T, E> 让错误处理类型化。这些模式是 Vue3/Pinia 等现代框架源码的日常——学完今天,你读框架源码的类型部分不再发怵。
目录
一、泛型接口:从数据结构到协议
1.1 数据结构层:Page<T> 分页协议
几乎每个列表接口都长这样:
/**
* 分页结构:任何列表数据的通用"信封"
*/
interface Page<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
// 设备分页
const devicePage: Page<Device> = {
list: [device1, device2],
total: 128,
page: 1,
pageSize: 20
};
devicePage.list[0].temp; // ✅ Device 字段精确
// 告警分页 —— 同一个信封
const alertPage: Page<AlertRecord> = { list: [alert], total: 1, page: 1, pageSize: 20 };
alertPage.list[0].level; // ✅
// 工单分页、用户分页…… 全项目统一
价值:后端同学看到 Page<T> 就知道分页协议;前端拿到 Page<Device> 就知道 list 里是什么——接口即文档。
1.2 响应协议层:ApiResponse<T>
/**
* 统一响应协议(data 参数化)
*/
interface ApiResponse<T> {
code: number; // 0 = 成功
message: string;
data: T;
}
// 各接口的响应类型一行派生:
type DeviceListRes = ApiResponse<Device[]>;
type StatsRes = ApiResponse<DeviceStats>;
type LoginRes = ApiResponse<{ token: string }>;
// ⭐ 实体改字段 → 所有响应类型自动同步(单一真相源的协议版)
1.3 行为协议层:泛型方法签名
泛型接口不只是"数据的形状",还能定义"行为的标准":
/**
* 可序列化协议:任何实现者都要提供序列化能力
*/
interface Serializable<T> {
serialize(): string;
deserialize(raw: string): T | null;
}
/**
* 可比较协议
*/
interface Comparable<T> {
compareTo(other: T): number; // 负数=小于,0=等于,正数=大于
}
// 实现协议(Device 仓储具备序列化能力)
class DeviceStore implements Serializable<Device[]> {
private devices: Device[] = [];
serialize(): string {
return JSON.stringify(this.devices);
}
deserialize(raw: string): Device[] | null {
try {
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed) ? parsed as Device[] : null;
// ⚠ as 断言是简化:严格版应过守卫(D4 知识)
} catch {
return null;
}
}
}
1.4 函数类型的泛型接口
/**
* 泛型函数类型的接口形态(等价于 type Fn = <T>(...) => ...)
*/
interface Mapper<T, R> {
(item: T, index: number): R;
}
// 使用(Array.map 的参数就是这个形状)
const devices: Device[] = [];
const names: string[] = devices.map((d: Mapper<Device, string>) => d.name);
二、泛型类:状态持有类型参数
2.1 第一个泛型类:栈
/**
* 泛型栈:LIFO 容器
* 类声明 <T> 后,T 在整个类体内可用(属性、方法、构造器参数)
*/
class Stack<T> {
private items: T[] = [];
/** 入栈 */
push(item: T): void {
this.items.push(item);
}
/** 出栈(空栈返回 undefined) */
pop(): T | undefined {
return this.items.pop();
}
/** 看栈顶不出栈 */
peek(): T | undefined {
return this.items[this.items.length - 1];
}
/** 当前大小 */
get size(): number {
return this.items.length;
}
}
// ===== 使用:数字栈 =====
const numStack = new Stack<number>();
numStack.push(65);
numStack.push(42);
const top = numStack.pop(); // number | undefined
// numStack.push("hot"); // ❌ 编译错误
// ===== 设备栈(另一个实例化)=====
const deviceStack = new Stack<Device>();
deviceStack.push(device1);
deviceStack.peek()?.temp; // ✅
2.2 实例化即锁定
// ⭐ 核心机制:new Stack<number>() 的那一刻,T 被锁定为 number
// 这个实例的所有方法签名都按 number 实例化:
const s = new Stack<number>();
s.push; // (item: number) => void
s.pop; // () => number | undefined
// 不同实例互不干扰
const s2 = new Stack<string>();
s2.push("hello");
// s2.push(123); // ❌ 各自的 T 各自锁定
2.3 泛型类 vs 泛型函数的推断差异
// 函数:每次调用独立推断
function identity<T>(v: T): T { return v; }
identity(1); // T = number
identity("a"); // T = string(下一次调用可以变)
// 类:new 时推断一次,终身绑定
const stack = new Stack([65, 42]); // 推断 T = number(从构造实参)
stack.push("x"); // ❌ 已经锁死 number
// 想换类型 → new 一个新实例
三、Repository 模式:约束的框架级应用
3.1 完整实现
/**
* 通用仓库:任何"有 id 的实体"的增删改查
* ⭐ 后端框架(NestJS TypeORM)和前端状态管理的核心模式
*/
interface HasId {
id: string;
}
class Repository<T extends HasId> {
private store = new Map<string, T>();
constructor(initial: T[] = []) {
for (const item of initial) {
this.store.set(item.id, item);
}
}
/** 新增(id 重复则覆盖) */
add(item: T): void {
this.store.set(item.id, item);
}
/** 按 id 查 */
get(id: string): T | undefined {
return this.store.get(id);
}
/** 部分更新(Partial<T>:D11 预告的工具类型) */
update(id: string, patch: Partial<T>): T | undefined {
const item = this.store.get(id);
if (!item) return undefined;
const updated = { ...item, ...patch };
this.store.set(id, updated);
return updated;
}
/** 删除 */
remove(id: string): boolean {
return this.store.delete(id);
}
/** 全量列表 */
list(): T[] {
return [...this.store.values()];
}
/** 按条件过滤(回调参数类型 = T) */
filter(fn: (item: T) => boolean): T[] {
return this.list().filter(fn);
}
/** 数量 */
get count(): number {
return this.store.size;
}
}
3.2 一个类服务所有实体
// ===== 设备仓库 =====
const deviceRepo = new Repository<Device>(devices);
deviceRepo.add({ id: "CNC-003", name: "机床3", temp: 55 });
const found = deviceRepo.get("CNC-001"); // Device | undefined
deviceRepo.update("CNC-001", { temp: 88 }); // ✅ Partial<Device>
const hot = deviceRepo.filter(d => d.temp > 80); // Device[]
// ===== 告警仓库(同一个类!)=====
const alertRepo = new Repository<AlertRecord>();
alertRepo.update("1", { reason: "已确认" }); // ✅ Partial<AlertRecord>
// ===== 阶段1 对比 =====
// 你在阶段1 为设备手写 store.js、为告警手写 alertList 管理 —— 每种数据一套代码
// 现在:一个泛型类 + 类型参数,全部搞定
3.3 Repository 的分层意义
index.ts / 组件层
↓ 只看到方法
DataHub(业务逻辑:告警判定、统计)
↓ 委托存储
Repository<T>(通用存储:增删改查)
↓
Map<string, T>(数据结构)
这就是"框架感":通用的下层 + 业务的上层,通过泛型类型参数衔接——你写的 Repository 和 VueUse 的 useStorage、Pinia 的 defineStore 在架构上是同一物种。
四、Result 模式:错误处理类型化
4.1 null 的问题
// 第 7 天 dataHub 的 ingest 返回 null 表示失败:
function ingest(payload: unknown): { count: number } | null { /* ... */ }
const r = hub.ingest(payload);
if (!r) {
// ⚠ 失败原因丢了!是解析失败?网络错误?结构不对?
console.error("失败了,但不知道为什么");
}
null 只能表达"失败",丢失了失败的信息。Result 模式补上这一环。
4.2 Result 类型定义
/**
* Rust 风格 Result:成功/失败都有类型化的载荷
*/
type Result<T, E = Error> =
| { ok: true; value: T } // 成功分支
| { ok: false; error: E }; // 失败分支
// 构造函数(让创建更顺手)
const ok = <T>(value: T): Result<T> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
4.3 使用:判别联合 + 泛型联手
/** 解析错误的类型化描述 */
interface ParseError {
code: "PARSE_FAIL" | "EMPTY_DATA" | "BAD_STRUCTURE";
reason: string;
}
function ingest(payload: unknown): Result<{ count: number }, ParseError> {
const result = parsePayload(payload);
if (!result.ok) {
return err({ code: "PARSE_FAIL", reason: result.reason });
}
if (result.devices.length === 0) {
return err({ code: "EMPTY_DATA", reason: "没有合法设备" });
}
return ok({ count: result.devices.length });
}
// ===== 调用方:类型安全地处理两种结果 =====
const r = hub.ingest(payload);
if (r.ok) {
console.log(`入库 ${r.value.count} 台`); // ✅ r.value: { count: number }
} else {
// r.ok === false → 自动收窄到 error 分支(D3 判别联合 + D4 收窄!)
console.error(r.error.code, r.error.reason); // ✅ 字段精确
if (r.error.code === "BAD_STRUCTURE") {
// 相等收窄再进一步(D4)
console.warn("建议检查后端版本");
}
}
知识合流时刻:Result = 第 3 天判别联合 + 第 4 天收窄 + 今天的泛型。它不是新知识,是三大知识的最优雅组合。
4.4 async 版本
/**
* 安全的 JSON 解析(永不 throw 的版本)
*/
function safeParse<T>(raw: string): Result<T, SyntaxError> {
try {
return ok(JSON.parse(raw) as T);
} catch (e) {
return err(e instanceof SyntaxError ? e : new SyntaxError(String(e)));
}
}
const parsed = safeParse<Device[]>(raw);
if (!parsed.ok) {
console.error("JSON 损坏:", parsed.error.message);
}
五、多类型参数与默认值
5.1 双参数类:BiMap(双向映射)
/**
* 双向映射表:K → V 和 V → K 双向可查
*/
class BiMap<K extends string, V extends string> {
private forward = new Map<K, V>();
private backward = new Map<V, K>();
set(key: K, value: V): void {
this.forward.set(key, value);
this.backward.set(value, key);
}
getByKey(key: K): V | undefined {
return this.forward.get(key);
}
getByValue(value: V): K | undefined {
return this.backward.get(value);
}
}
// 状态码 ↔ 状态名的双向表
const statusCode = new BiMap<"running" | "fault", string>();
statusCode.set("running", "运行中");
statusCode.set("fault", "故障");
statusCode.getByKey("running"); // "运行中"
statusCode.getByValue("故障"); // "running"
5.2 默认类型参数
/**
* 默认参数:简单场景少传一个类型
*/
interface Paged<T, M = Record<string, unknown>> {
list: T[];
total: number;
meta?: M; // 可选的元信息,默认宽类型
}
type Simple = Paged<Device>; // M 用默认
type Detailed = Paged<Device, { cursor: string }>; // 自定义 meta
5.3 参数间的依赖
/**
* 类型参数间的引用:K 依赖 T
*/
type FieldSelector<T, K extends keyof T = keyof T> = {
field: K;
getValue: (item: T) => T[K];
};
// 不传 K:默认全部字段可选
type AnyField = FieldSelector<Device>;
// 传 K:锁定单字段
type TempField = FieldSelector<Device, "temp">;
// TempField 的 getValue: (item: Device) => number
六、泛型类的继承与实现
6.1 泛型类继承泛型类
/**
* 基础仓库(通用能力)
*/
class BaseRepo<T extends HasId> {
protected store = new Map<string, T>();
get(id: string): T | undefined { return this.store.get(id); }
list(): T[] { return [...this.store.values()]; }
}
/**
* 设备仓库:继承时【填充】类型参数
*/
class DeviceRepo extends BaseRepo<Device> {
/** 子类加业务方法 —— this 继承的类型全部按 Device 实例化 */
getOverheated(): Device[] {
return this.list().filter(d => d.temp >= 80);
}
/** 更新温度(Device 专属方法,泛型基类写不了)*/
updateTemp(id: string, temp: number): void {
const d = this.get(id);
if (d) this.store.set(id, { ...d, temp });
}
}
const repo = new DeviceRepo();
repo.getOverheated()[0]?.temp; // ✅ Device 字段
6.2 泛型类继承时保持参数化
/**
* 带索引的仓库:继承时【保持】类型参数(继续传给基类)
*/
class IndexedRepo<T extends HasId> extends BaseRepo<T> {
private indexes = new Map<keyof T, Map<unknown, T[]>>();
/** 建立字段索引(O(1) 查询的预处理) */
buildIndex<K extends keyof T>(field: K): void {
const index = new Map<unknown, T[]>();
for (const item of this.list()) {
const key = item[field];
const list = index.get(key);
if (list) list.push(item);
else index.set(key, [item]);
}
this.indexes.set(field, index);
}
/** 走索引查询 */
findBy<K extends keyof T>(field: K, value: T[K]): T[] {
return this.indexes.get(field)?.get(value) ?? [];
}
}
const indexed = new IndexedRepo<Device>();
indexed.buildIndex("status");
indexed.findBy("status", "running"); // Device[](D9 黄金组合在类方法里)
6.3 实现泛型接口
/** 存储协议 */
interface Storable<T> {
read(): T | null;
write(value: T): void;
clear(): void;
}
/** localStorage 实现 */
class LocalStorage<T> implements Storable<T> {
constructor(private key: string) {}
read(): T | null {
const raw = localStorage.getItem(this.key);
if (raw === null) return null;
try { return JSON.parse(raw) as T; } catch { return null; }
}
write(value: T): void {
localStorage.setItem(this.key, JSON.stringify(value));
}
clear(): void {
localStorage.removeItem(this.key);
}
}
// ⚠ 类实现泛型接口时,类型参数必须对得上:
class BadRepo implements Storable<Device> {
read(): Device | null { return null; } // ✅ T 被填成 Device
write(v: Device): void {} // ✅
clear(): void {}
}
七、实战场景全覆盖
7.1 场景一:泛型分页器(大屏滚动列表)
/**
* 分页器:滚动加载的核心逻辑
*/
class Paginator<T> {
private cursor = 0;
constructor(
private items: T[],
private pageSize: number
) {}
/** 下一页 */
next(): T[] {
const page = this.items.slice(this.cursor, this.cursor + this.pageSize);
this.cursor += this.pageSize;
return page;
}
/** 还有下一页吗 */
get hasNext(): boolean {
return this.cursor < this.items.length;
}
/** 已加载百分比(进度条数据) */
get progress(): number {
return Math.min(100, Math.round((this.cursor / this.items.length) * 100));
}
/** 重置 */
reset(): void {
this.cursor = 0;
}
}
// 告警历史滚动加载
const pager = new Paginator(alertHistory, 10);
function loadMore(): void {
if (!pager.hasNext) return;
const batch = pager.next(); // AlertRecord[]
appendToDom(batch);
updateProgressBar(pager.progress);
}
7.2 场景二:泛型 WebSocket 消息管理器
/**
* WS 消息管理:消息类型与处理器类型绑定
*/
type WsMessageMap = {
"device:data": Device[];
"alert:new": AlertRecord;
"conn:status": "open" | "close" | "error";
};
class WsManager<M extends Record<string, unknown>> {
private ws: WebSocket | null = null;
private handlers: {
[K in keyof M]?: ((payload: M[K]) => void)[];
} = {};
constructor(private url: string) {}
connect(): void {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (e: MessageEvent) => {
try {
const msg: unknown = JSON.parse(e.data);
if (!this.isValidMessage(msg)) return; // D4 守卫思想
const { event, payload } = msg as { event: keyof M; payload: M[keyof M] };
this.handlers[event]?.forEach(fn => fn(payload as never));
} catch {
console.warn("[ws] 非 JSON 消息被丢弃");
}
};
}
/** 简化的结构校验 */
private isValidMessage(v: unknown): boolean {
return typeof v === "object" && v !== null && "event" in v;
}
/** 订阅(D9 事件总线模式在类里的形态) */
on<K extends keyof M>(event: K, fn: (payload: M[K]) => void): void {
(this.handlers[event] ??= []).push(fn);
}
close(): void {
this.ws?.close();
this.ws = null;
}
}
// 使用
const ws = new WsManager<WsMessageMap>("ws://localhost:8080");
ws.on("device:data", list => renderDevices(list)); // list: Device[]
ws.on("alert:new", alert => pushAlert(alert)); // alert: AlertRecord
ws.connect();
7.3 场景三:泛型组合式函数形态(Vue3 预演)
/**
* 通用轮询逻辑(Vue3 组合式函数的"无框架版")
* 体会:泛型类 + 泛型函数 + 约束的组合,就是 usePolling 的骨架
*/
class Poller<T> {
private timer: number | undefined;
private last: T | null = null;
constructor(
private fetcher: () => Promise<T>,
private interval = 3000,
private onUpdate?: (data: T) => void
) {}
start(): void {
const run = async () => {
try {
const data = await this.fetcher();
this.last = data;
this.onUpdate?.(data);
} catch (e) {
console.error("[poller] 拉取失败", e);
}
};
run();
this.timer = setInterval(run, this.interval) as unknown as number;
}
stop(): void {
if (this.timer !== undefined) clearInterval(this.timer);
this.timer = undefined;
}
/** 最近一次数据 */
get latest(): T | null {
return this.last;
}
}
// 设备轮询
const poller = new Poller<Device[]>(
() => request<Device[]>("/api/devices") ?? Promise.resolve([]),
3000,
devices => renderDevices(devices)
);
poller.start();
// 第 29 周的 usePolling(composable) 与此结构几乎一致 —— 只是宿主从 class 换成 setup 函数
7.4 场景四:泛型单例容器(依赖注入雏形)
/**
* 类型安全的服务容器:注册什么类型,取出什么类型
*/
class ServiceContainer {
private services = new Map<string, unknown>();
/** 注册服务(泛型捕获注册时的类型) */
register<T>(key: string, instance: T): void {
this.services.set(key, instance);
}
/** 取出服务(显式指定类型 —— 无法推断的场景) */
resolve<T>(key: string): T | undefined {
return this.services.get(key) as T | undefined;
}
}
const container = new ServiceContainer();
container.register("deviceRepo", new Repository<Device>(devices));
container.register("eventBus", new EventBus<HubEvents>());
const repo = container.resolve<Repository<Device>>("deviceRepo");
repo?.get("CNC-001")?.temp; // ✅
// 这就是 Vue3 的 provide/inject、Angular 依赖注入的类型原理
八、类比记忆:集装箱体系
九、常见坑点与最佳实践
坑点 1:静态成员访问类型参数
class Box<T> {
static defaultValue: T; // ❌ 编译错误
static create(): T { /* ... */ } // ❌
// 原因:类型参数属于【实例】,static 属于【类本身】
// 类的静态侧在实例化之前就存在 —— 那时 T 还没确定
}
// ✅ 变通:静态方法用自己的泛型
class Box2<T> {
static of<V>(value: V): Box2<V> {
return new Box2<V>(value);
}
constructor(public value: T) {}
}
坑点 2:new 时忘记传类型参数且推断不出
class Cache<V> {
private store = new Map<string, V>();
get(key: string): V | undefined { return this.store.get(key); }
}
const cache = new Cache(); // V = unknown(推断不出,悄悄退化!)
cache.get("x"); // unknown —— 后续全是 as
// ✅ 显式指定
const cache2 = new Cache<Device[]>();
坑点 3:泛型类的字段类型被推断拓宽
class Stack<T> {
private items: T[] = [];
// ⚠ 如果构造函数不接收 T 的数据,T 无法从构造实参推断
// new Stack() 时 T = unknown
}
// ✅ 让构造函数携带可推断的 T:
class Stack2<T> {
constructor(private items: T[] = []) {}
}
const s = new Stack2([1, 2, 3]); // T = number ✅
坑点 4:泛型接口实现时类型不匹配
interface Storable<T> {
read(): T | null;
}
// ❌ 实现时换了类型
class Bad implements Storable<Device> {
read(): AlertRecord | null { return null; } // 编译错误
}
// ✅ 接口的类型参数必须被【如实履行】
坑点 5:误以为泛型类有运行时开销
// ⚠ 泛型是编译期机制:Stack<number> 和 Stack<string> 在 JS 里是同一个类
// 类型参数在运行时被完全擦除(昨天知识在类上的延续)
// 不存在"泛型反射"、不能 new T():
class Factory<T> {
create(): T {
return new T(); // ❌ 编译错误:T 只是类型
}
// ✅ 想创建实例 → 传构造函数进来(依赖注入思想)
createVia<Ctor extends new () => T>(ctor: Ctor): T {
return new ctor();
}
}
最佳实践清单
new 泛型类时显式传类型参数(除非构造实参能推断)
Repository 模式优先于每实体手写 store——一次编写全实体复用
Result 替代 null 返回——失败信息类型化(判别联合 + 泛型)
协议(泛型接口)与实现(泛型类)分离——Storable + LocalStorage<T>
静态方法不能用类级 T——需要泛型就自己声明
不能 new T()——传构造函数(类型依赖注入)
继承时想清楚:填充参数(子类特化)还是保持参数化(继续通用)
十、自测挑战
Q1:new Stack<number>() 之后,push 的参数类型是什么?为什么终身锁定?
Q2:泛型类和泛型函数在"类型推断时机"上的差异?
Q3:Repository 为什么用 T extends HasId 而不是直接为 Device 写仓库类?说出两点收益。
Q4:update(id: string, patch: Partial<T>) 里的 Partial 起什么作用?
Q5:Result 模式由哪三个已学知识组合而成?分别解决什么?
Q6:为什么类的静态成员不能使用类型参数?
Q7:class DeviceRepo extends BaseRepo<Device> 和 class IndexedRepo<T> extends BaseRepo<T> 的区别是什么?各自什么场景用?
Q8:泛型类里想 new T() 为什么不行?正确的做法?
Q9:手写泛型类 Queue<T>(FIFO:enqueue/dequeue/front/size)。
Q10:手写 Result 版 safeDivide(a: number, b: number): Result<number, string>(除零返回错误)。
Q11:设计 ServiceContainer:register 时泛型捕获类型,resolve 时如何保证类型安全?这个方案的弱点是什么(提示:resolve 靠什么)?
十一、总结与知识图谱
泛型接口与泛型类(类型工厂的工程化)
│
├── 泛型接口
│ ├── 数据协议:Page<T> / ApiResponse<T>
│ ├── 行为协议:Serializable<T> / Comparable<T>
│ └── 函数类型接口:Mapper<T, R>
│
├── 泛型类
│ ├── Stack<T>(实例化即锁定)
│ ├── 推断时机:new 一次(vs 函数每次调用)
│ └── ⚠ 静态成员禁用 T / 不能 new T()
│
├── 架构模式(今天的主菜)
│ ├── Repository<T extends HasId>
│ │ └── 增删改查 + filter + Partial 更新
│ ├── Result<T, E = Error>
│ │ └── 判别联合 + 收窄 + 泛型 = 错误类型化
│ ├── Paginator<T>(滚动分页)
│ ├── WsManager<M extends Record<...>>(消息绑定)
│ └── ServiceContainer(依赖注入雏形)
│
├── 参数进阶
│ ├── 多参数 BiMap<K, V>
│ ├── 默认值 <T, E = Error>(放最后)
│ └── 参数间依赖 <T, K extends keyof T = keyof T>
│
└── 继承与实现
├── extends BaseRepo<Device>(填充:子类特化)
├── extends BaseRepo<T>(保持:继续通用)
└── implements Storable<T>(如实履约)
一句话总结:泛型接口定义"协议",泛型类实现"机制",类型参数是两者之间的插座——Repository 和 Result 这两个模式,就是你项目里第一个"框架级"代码。
延伸阅读
下一步
本文是 TypeScript 深入 系列的第 10 天。接下来:
第 11 天:内置工具类型全解 — Partial/Pick/Omit/Record/ReturnType 全家桶实战(你将看清官方"预制集装箱"的内部结构)
学编程就像蜗牛往上爬,慢一点没关系,关键是不停下来。
每天花 2 小时,28 天通关 TypeScript 深入。加油!