TypeScript 高级类型:我用 infer 写了一个类型安全的 EventBus,终于搞懂了泛型约束

TypeScript 写了五年,any 也用了五半。直到被迫写一个类型安全的 EventBus,我才真正搞懂 infer、extends、keyof 和泛型约束。本文从实际场景出发,一步步推导出类型安全的 API。

一、痛点:EventBus 的类型安全问题

写一个简单的 EventBus:

typescript 复制代码
class EventBus {
  private listeners: Record<string, Function[]> = {};

  on(event: string, callback: Function) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event].push(callback);
  }

  emit(event: string, ...args: any[]) {
    (this.listeners[event] || []).forEach(cb => cb(...args));
  }
}

问题on('userLogin', (name: string) => {})emit('userLogin', 123) 类型不匹配,但 TS 不会报错。any[] 毁掉了所有类型安全。

目标 :让 onemit 的事件名和参数类型自动关联。

二、核心工具:泛型 + 映射类型

2.1 定义事件映射

typescript 复制代码
interface EventMap {
  userLogin: (name: string, age: number) => void;
  userLogout: () => void;
  dataUpdate: (data: { id: number }) => void;
}

2.2 提取参数类型(infer 闪亮登场)

typescript 复制代码
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

infer P 的意思是"在符合这个形状的地方,把参数类型推断出来赋值给 P"。

2.3 实现类型安全的 EventBus

typescript 复制代码
class TypedEventBus<T extends Record<string, (...args: any[]) => any>> {
  private listeners: {
    [K in keyof T]?: T[K][];
  } = {};

  on<K extends keyof T>(event: K, callback: T[K]) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event]!.push(callback);
  }

  emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>) {
    (this.listeners[event] || []).forEach(cb => cb(...args));
  }
}

2.4 使用效果

typescript 复制代码
const bus = new TypedEventBus<EventMap>();

bus.on('userLogin', (name, age) => {
  console.log(name, age); // name: string, age: number
});

bus.emit('userLogin', '张三', 25); // ✅ 类型正确
bus.emit('userLogin', 123);        // ❌ TS 报错:参数类型不匹配
bus.emit('userLogout');            // ✅ 无参数

三、5 个最实用的高级类型技巧

技巧 1:keyof 提取对象的键

typescript 复制代码
type Keys = keyof { name: string; age: number }; // 'name' | 'age'

技巧 2:typeof 获取值的类型

typescript 复制代码
const config = { host: 'localhost', port: 3000 };
type Config = typeof config; // { host: string; port: number }

技巧 3:infer 提取函数返回类型

typescript 复制代码
type ReturnType<T> = T extends (...args: any) => infer R ? R : never;

技巧 4:extends 约束泛型

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

技巧 5:in 映射类型

typescript 复制代码
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

四、完整 EventBus 代码(可直接复制)

typescript 复制代码
// 类型安全的 EventBus
interface EventMap {
  [event: string]: (...args: any[]) => void;
}

class TypedEventBus<T extends EventMap> {
  private listeners: {
    [K in keyof T]?: T[K][];
  } = {};

  on<K extends keyof T>(event: K, callback: T[K]) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

  off<K extends keyof T>(event: K, callback: T[K]) {
    const callbacks = this.listeners[event];
    if (!callbacks) return;
    const index = callbacks.indexOf(callback);
    if (index !== -1) callbacks.splice(index, 1);
  }

  emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>) {
    const callbacks = this.listeners[event];
    if (!callbacks) return;
    callbacks.forEach(cb => cb(...args));
  }

  once<K extends keyof T>(event: K, callback: T[K]) {
    const wrapper = (...args: Parameters<T[K]>) => {
      callback(...args);
      this.off(event, wrapper as T[K]);
    };
    this.on(event, wrapper as T[K]);
  }
}

五、总结

  • infer 是高级类型中最强大的工具,用于在条件类型中提取类型信息
  • keyof + extends 可以实现类型安全的键值访问
  • typeof 可以将 JS 值转为 TS 类型
  • EventBus 的类型安全实现完整展示了这些工具的配合使用
相关推荐
L-影几秒前
FastAPI CORS 跨域:Web 世界的“小区门禁”与“访客通行证”
前端·javascript·fastapi
stereohomology6 分钟前
已进入初赛提交Demo的两个项目:(2)单独html钢琴
前端·html·why不coding
源图客13 分钟前
Claude Code基础使用
服务器·前端·数据库
asdzx6729 分钟前
在 React 中轻松实现 PDF 转 Word:纯前端 WASM 方案实战
前端·react.js·pdf
爱勇宝9 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
梨子同志10 小时前
常用命令
前端
梨子同志10 小时前
React 状态管理
前端
Dxy123931021610 小时前
MySQL索引完整教程:创建、查看、修改、删除与日常管理
前端·javascript·mysql
剪刀石头布啊11 小时前
js能被遍历的集合有哪些特征,为何能被遍历
前端
剪刀石头布啊11 小时前
js解构
前端