【TS】day10-generic-interfaces-classes

作者:mario 发布时间: 2026-08-28 阅读量:6 评论数:0

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 依赖注入的类型原理

八、类比记忆:集装箱体系

概念

类比

说明

Page<T>

标准集装箱

外壳统一(分页协议),内容随填

new Stack<number>()

集装箱贴上编号

实例化即锁定,终身不改

Repository<T extends HasId>

海关通关章程

有护照(id)的货物才能走这条通道

Result<T, E>

双通道分拣系统

成功走 value 通道,失败走 error 通道,各有标签

泛型继承 extends BaseRepo<Device>

子公司继承总公司的集装箱规格,并指定货物类型

填充参数

泛型继承 extends BaseRepo<T>

子公司继续做通用物流

保持参数化

默认类型参数

不注明货物品类时按普货处理

兜底配置

ServiceContainer

保税仓登记簿

登记什么类型,提取时对号入座


九、常见坑点与最佳实践

坑点 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();
  }
}

最佳实践清单

  1. new 泛型类时显式传类型参数(除非构造实参能推断)

  2. Repository 模式优先于每实体手写 store——一次编写全实体复用

  3. Result 替代 null 返回——失败信息类型化(判别联合 + 泛型)

  4. 协议(泛型接口)与实现(泛型类)分离——Storable + LocalStorage<T>

  5. 静态方法不能用类级 T——需要泛型就自己声明

  6. 不能 new T()——传构造函数(类型依赖注入)

  7. 继承时想清楚:填充参数(子类特化)还是保持参数化(继续通用)


十、自测挑战

Q1new Stack<number>() 之后,push 的参数类型是什么?为什么终身锁定?

Q2:泛型类和泛型函数在"类型推断时机"上的差异?

Q3:Repository 为什么用 T extends HasId 而不是直接为 Device 写仓库类?说出两点收益。

Q4update(id: string, patch: Partial<T>) 里的 Partial 起什么作用?

Q5:Result 模式由哪三个已学知识组合而成?分别解决什么?

Q6:为什么类的静态成员不能使用类型参数?

Q7class DeviceRepo extends BaseRepo<Device>class IndexedRepo<T> extends BaseRepo<T> 的区别是什么?各自什么场景用?

Q8:泛型类里想 new T() 为什么不行?正确的做法?

Q9:手写泛型类 Queue<T>(FIFO:enqueue/dequeue/front/size)。

Q10:手写 ResultsafeDivide(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 这两个模式,就是你项目里第一个"框架级"代码。


延伸阅读

资源

说明

TS Handbook - Generic Classes

官方泛型类文档

Vue3 源码 - packages/reactivity

泛型接口/类的工业级应用(有难度,看结构即可)


下一步

本文是 TypeScript 深入 系列的第 10 天。接下来:

  • 第 11 天:内置工具类型全解 — Partial/Pick/Omit/Record/ReturnType 全家桶实战(你将看清官方"预制集装箱"的内部结构)


学编程就像蜗牛往上爬,慢一点没关系,关键是不停下来。

每天花 2 小时,28 天通关 TypeScript 深入。加油!


评论