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[] 毁掉了所有类型安全。

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

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

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 的类型安全实现完整展示了这些工具的配合使用
相关推荐
害人终害己8 小时前
‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。
前端·javascript·vue.js
被摘下的星星8 小时前
CSS 基础说明
前端·css
Flynt9 小时前
SvelteKit 3 升级实录:sv migrate 改完 6 个源文件,还给我留了张任务清单
typescript·前端框架·svelte
To_OC18 小时前
从一头雾水到跑通全流程:我用一个周末啃透了JWT登录鉴权
前端·后端·http
excel20 小时前
研究 Vue 3 源码的收获
前端·vue.js
可乐鸡翅yeah_1 天前
业务中 M3U8 水印相关坑,硬水印和动态水印区别
前端·网络·数据库·ffmpeg·m3u8在线
lerhxx1 天前
AI 应用如何高效优雅地恢复中断?—— "连接解耦 + 状态持久化"
前端·javascript
计算机魔术师1 天前
METR 演示 AI 智能体如何篡改 Inspect 评估记录以掩盖不当行为
前端
前端snow1 天前
ai agent --- 异步处理之 Rabbit MQ
前端
念何架构之路1 天前
zap扩展生态与总结
java·前端·数据库