TypeScript如何实现类型安全的EventEmitter
这篇文章主要介绍了TypeScript如何实现类型安全的EventEmitter的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇TypeScript如何实现类型安全的EventEmitter文章都会有所收获,下面我们一起来看看吧。
Nodejs 的 EventEmitter 是一个发布订阅模块。
利用该类,我们可以实现事件的监听,被监听对象会在合适的时机触发事件,调用监听对象提供的方法,是模块间解耦的常用实现。
配合越来越流行的 TypeScript,我们可以通过安装 @types/node
,我们能够进一步获得类型能力,减少低级错误的出现。但 EventEmitter 的类型实现并不出色,称不上是类型安全。
通常来说,不同事件对应的响应函数类型是不同的,但 @types/node
的 EventEmiiter 类型没有提供高级类型,而是给一个异常宽松的值。
class EventEmitter { constructor(options?: EventEmitterOptions); // 类型过于宽泛 on(eventName: string | symbol, listener: (...args: any[]) => void): this; emit(eventName: string | symbol, ...args: any[]): boolean; // ...其他 }
可以看到,on 方法传入的事件名类型是 string | symbol
,listener 则是随意任何类型的一个函数即可。emit 传入的参数也是 any[]
。
因为过于宽松的类型,如果事件名拼错了,TypeScript 并不会报错,当一个 eventEmitter 的事件类型变得非常多,我们就和裸写 JavaScript 没什么区别了。
自己动手,丰衣足食,我们不妨 自己实现一个类型安全的 EventEmitter。
EventEmitter 实现
因为我其实是在前端用的 EventEmitter,所以写了一个 EventEmitter 简易 JavaScript 实现。
class EventEmitter { eventMap = {}; // 添加对应事件的监听函数 on(eventName, listener) { if (!this.eventMap[eventName]) { this.eventMap[eventName] = []; } this.eventMap[eventName].push(listener); return this; } // 触发事件 emit(eventName, ...args) { const listeners = this.eventMap[eventName]; if (!listeners || listeners.length === 0) return false; listeners.forEach((listener) => { listener(...args); }); return true; } // 取消对应事件的监听 off(eventName, listener) { const listeners = this.eventMap[eventName]; if (listeners && listeners.length > 0) { const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } return this; } }
如果你是 nodejs,继承 EventEmitter 然后改它的类型或许是更好的做法,或者可以 “基于组合而不是继承” 的方式实现一个。
类型安全的 EventEmitter
接着是将上面的代码改为 TypeScript。
我们希望的效果是:
const ee = new EventEmitter<{ update(newVal: string, prevVal: string): void; destroy(): void; }>(); const handler = (newVal: string, prevVal: string) => { console.log(newVal, prevVal) } ee.on("update", handler); ee.emit('update', '前端西瓜哥上班前的精神状态', '前端西瓜哥上班后的精神状态') ee.off("update", handler); // 以下报错 // 'number' is not assignable to parameter of type 'string' ee.emit('update', 1, 2) // (val: number) => void' is not assignable to parameter of type '() => void ee.on('destroy', (val: number) => {})
EventEmitter 支持接受一个对象结构的 interface 作为类型参数,指定不同的 key 对应的函数类型。
然后我们再调用 on、emit、off 时,如果事件名、函数参数不匹配,编译就不能通过。
代码实现:
class EventEmitter<T extends Record<string | symbol, any>> { private eventMap: Record<keyof T, Array<(...args: any[]) => void>> = {} as any; // 添加对应事件的监听函数 on<K extends keyof T>(eventName: K, listener: T[K]) { if (!this.eventMap[eventName]) { this.eventMap[eventName] = []; } this.eventMap[eventName].push(listener); return this; } // 触发事件 emit<K extends keyof T>(eventName: K, ...args: Parameters<T[K]>) { const listeners = this.eventMap[eventName]; if (!listeners || listeners.length === 0) return false; listeners.forEach((listener) => { listener(...args); }); return true; } // 取消对应事件的监听 off<K extends keyof T>(eventName: K, listener: T[K]) { const listeners = this.eventMap[eventName]; if (listeners && listeners.length > 0) { const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } return this; } }
读者朋友可自行拷贝上面两段代码到 TypeScript Playground 测试一下。
简单讲解一下。
首先是开头的类型参数。
class EventEmitter<T extends Record<string | symbol, any>> { // }
这里的 extends 作用是限定类型范围,防止提供一个不符合规则的类型参数。
Record 是 TypeScript 自带的高级类型,根据传入的 key 和 value 创建一个对象结构(后面说到的 T 就是它)。
Record<string | symbol, any> // 等价于 { [key: string | symbol]: any }
value 本来的类型应该是 (...args: any[]) => void
,好限制为函数。但在不是非字面量类型直传的情况下无法通过类型检测,只好改成 any 了。(坑爹的 Index signature for type 'string' is missing
报错)
然后是 eventMap,它的实际内容是这样的:
eventMap = { event1: [ handler1, handler2 ], event2: [ handler3, handler4 ] }
所以 key 需要为传入对象类型参数的 key。
函数则不用指定特定类型,因为它是私有的,无法被类外部访问,没有做过多的类型推断,就宽松一些,设置为任何函数类型。
private eventMap: Record<keyof T, Array<(...args: any[]) => void>> = {} as any;
这里我用了对象字面量,读者朋友也可以考虑用 Map 数据结构。
然后是 on 方法,首先 eventName 必须为 T 的 key 的其中之一,因为要推断 K 这么个内部类型变量,所以我们要在 on 后面加上 <K extends keyof T>
,listener 就是对应的 T[K]
。
on<K extends keyof T>(eventName: K, listener: T[K]): this
off 方法同理,不展开讲。
然后是 emit,第一个 eventName 用 keyof T
没问题,后面需要取出 handler 的参数,作为剩余参数。
emit<K extends keyof T>(eventName: K, ...args: Parameters<T[K]>): boolean
这里用了 TS 自带的 Parameters 高级类型,作用是取出函数的参数返回一个数组类型。
临时扩展自定义事件
如果要给一个已经固定了类型的实例,临时加一个事件,可以用 &
交叉类型扩展一下。
interface Events { update(newVal: string, prevVal: string): void; destroy(): void; } const ee = new EventEmitter<Events>(); // 用 & 扩展 const ee2 = ee as EventEmitter< Events & { customA(a: boolean): void; } >; // 不报错 ee2.emit('customA', true) // 或者 (ee as EventEmitter< Events & { customA(a: boolean): void; } >).emit('customA', true)
关于“TypeScript如何实现类型安全的EventEmitter”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“TypeScript如何实现类型安全的EventEmitter”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注蜗牛博客行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:niceseo99@gmail.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
评论