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 的类型安全实现完整展示了这些工具的配合使用