TypeScript实现类型安全的EventEmitter

目录
  • 正文
  • EventEmitter 实现
  • 类型安全的 EventEmitter
  • 临时扩展自定义事件
  • 结尾

正文

最近个人项目用 EventEmitter 模块越来越多了,因为类型不够安全,写起来要很小心。所以打算改良一下,实现 TypeScript 类型安全的 EventEmitter,解决事件名和函数类型不能做检验的问题。

Nodejs 的 EventEmitter 是一个发布订阅模块。

利用该类,我们可以实现事件的监听,被监听对象会在合适的时机触发事件,调用监听对象提供的方法,是模块间解耦的常用实现。

配合越来越流行的 TypeScript,我们可以通过安装 @types/node,我们能够进一步获得类型能力,减少低级错误的出现。但 EventEmitter 的类型实现并不出色,称不上是类型安全。

通常来说,不同事件对应的响应函数类型是不同的,但 @types/nodeEventEmiiter 类型没有提供高级类型,而是给一个异常宽松的值

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 的类型编程的语法真的很不美观,可读性差。如果你不是库作者,个人不建议过度使用类型体操,它像正则一样,很强大,但也很复杂。

以上就是TypeScript实现类型安全的EventEmitter的详细内容,更多关于TS EventEmitter安全类型的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解unplugin vue components不能识别组件自动导入类型pnpm

    目录 引言 效果 发现问题 问题效果 解决问题 刨根问底 解决方案 引言 unplugin-vue-components 是一款能帮助组件自动导入的库,简单点的说,你不需要使用import xx from 'xxx.vue' 这行语句也能实现导入的效果. <script setup lang="ts"> import ScreenAdpter from '@compontents/ScreenAdpter/index.vue' import Play from '@comp

  • 前端React Nextjs中的TS类型过滤实用技巧

    目录 自我介绍 分步介绍 开胃小菜 keyof in Conditional 泛型 正餐开始 实战应用例子 最后 大家好,我是零一,相信大家在阅读同事写的代码或者优秀的开源库的代码时,一定见过各种各样的风骚的TS写法,不花点时间下去根本看不懂,换作是我们,可能就直接一个 any 完事了,但是真正当项目体积变大后,你会发现这些 TS骚操作真的很重要,因为它能很好地帮助你做静态类型校验 自我介绍 TS类型过滤,英文名(我自己取的)叫 FilterConditionally,这是它完整的样子 type

  • UMD的包导出TS 类型方法示例

    目录 TypeScript 里声明模块 类型提示检查 UMD 的 global 类型 总结 TypeScript 里声明模块 在 TypeScript 里声明模块,最早是用 namespace 和 module 的语法,后来支持了 es module,类型和变量会用 import 来导入.用 export 导出. 比如你写了一个库,导出的变量叫 Guang,它下面有 name 和 age 两个属性,所以你是这样声明类型的: export default Guang; declare namesp

  • vue3+ts中ref与reactive指定类型实现示例

    目录 ref 的基础特性 如何在ref中指定类型 reactive isRef.isReactive toRef.toRefs.toRaw ref 的基础特性 ref 约等于 reactive({ value: x }) ref() 可以定义时无参数,第一次赋值任意类型,然后就不能增加属性 const refa = ref(6) const rcta = reactive({ value: 12 }) console.log('refa:', refa) //RefImpl{...} conso

  • 为Vue3 组件标注 TS 类型实例详解

    目录 为 props 标注类型 使用 <script setup> 非 <script setup> 为 emits 标注类型 使用 <script setup> 非 <script setup> 为 ref() 标注类型 默认推导类型 通过接口指定类型 通过泛型指定类型 为 reactive() 标注类型 默认推导类型 通过接口指定类型 为 computed() 标注类型 默认推导类型 通过泛型指定类型 为事件处理函数标注类型 为 provide / in

  • TypeScript实现类型安全的EventEmitter

    目录 正文 EventEmitter 实现 类型安全的 EventEmitter 临时扩展自定义事件 结尾 正文 最近个人项目用 EventEmitter 模块越来越多了,因为类型不够安全,写起来要很小心.所以打算改良一下,实现 TypeScript 类型安全的 EventEmitter,解决事件名和函数类型不能做检验的问题. Nodejs 的 EventEmitter 是一个发布订阅模块. 利用该类,我们可以实现事件的监听,被监听对象会在合适的时机触发事件,调用监听对象提供的方法,是模块间解耦

  • 使用TypeScript实现一个类型安全的EventBus示例详解

    目录 前言 准备工作 目标 思路 具体实现 全部代码 后记 前言 随着vue3的发布,TypeScript在国内越来越流行,学习TypeScript也随即变成了大势所趋.本文就通过实现一个类型安全的EventBus来练习TypeScript,希望对小伙伴们有所帮助. 准备工作 生成一个TypeScript的基础架子: // 创建目录 mkdir ts-event-bus && cd ts-event-bus // 初始化工程 yarn init -y // 安装typescript yar

  • 2022编程语言需求排名出炉:第一不是Python,也不是Java

    目录 1. JavaScript / TypeScript 2. Python 3. Java 4. C# 5. PHP 6. C/C++ 7. Ruby 8. GO 总结 编程语言的流行程度.发展前景.就业市场这些一直都是程序员们非常关注的话题,需求排名是程序员们关注学习的风向标,毕竟是市场经济,学以致用,如果热门编程不了解,都不好意思告诉别人你是程序员.编程语言的种类有超过200+,但还有很多不为人知. 2022年也快接近尾声,今年最受欢迎使用最多的语言有哪些呢? 从 2021 年 10 月

  • TypeScript Type Innference(类型判断)

    TypeScript 是微软开发的 JavaScript 的超集,TypeScript兼容JavaScript,可以载入JavaScript代码然后运行.TypeScript与JavaScript相比进步的地方 包括:加入注释,让编译器理解所支持的对象和函数,编译器会移除注释,不会增加开销:增加一个完整的类结构,使之更新是传统的面向对象语言. 为什么会有 TypeScript? JavaScript 只是一个脚本语言,并非设计用于开发大型 Web 应用,JavaScript 没有提供类和模块的概

  • 详解在Vue中使用TypeScript的一些思考(实践)

    Vue.extend or vue-class-component 使用 TypeScript 写 Vue 组件时,有两种推荐形式: Vue.extend():使用基础 Vue 构造器,创建一个"子类".此种写法与 Vue 单文件组件标准形式最为接近,唯一不同仅是组件选项需要被包裹在 Vue.extend() 中. vue-class-component:通常与 vue-property-decorator 一起使用,提供一系列装饰器,能让我们书写类风格的 Vue 组件. 两种形式输出

  • Vue+Typescript中在Vue上挂载axios使用时报错问题

    在vue项目开发过程中,为了方便在各个组件中调用axios,我们通常会在入口文件将axios挂载到vue原型身上,如下: main.ts import Vue from 'vue' import axios from './utils/http' Vue.prototype.$axios = axios; 这样的话,我们在各个组件中进行请求时,就可以直接使用this.$axios,但是在ts中使用this.$axios进行请求时,会进行报错,如下所示: 从图中我们可以看出ts在Vue身上检测不到

  • 浅谈Vue3.0之前你必须知道的TypeScript实战技巧

    很多人对TypeScript的使用还停留在基本操作上,其实TypeScript的特性非常强大,我们利用好这些特性可以有效地提高代码质量.加速开发效率,今天就介绍9个非常实用的TypeScript技巧或者特性. 注释的妙用 我们可以通过 /** */ 来注释TypeScript的类型,当我们在使用相关类型的时候就会有注释的提示,这个技巧在多人协作开发的时候十分有用,我们绝大部分情况下不用去花时间翻文档或者跳页去看注释. 巧用类型推导 TypeScript 能根据一些简单的规则推断(检查)变量的类型

  • 前端深入理解Typescript泛型概念

    首先介绍一下泛性的概念 泛型程序设计(generic programming)是程序设计语言的一种风格或范式.泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型. 泛型是指在定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性. 先举一个简单的例子 假设我们定义一个函数,它可以接收一个number类型做为参数,并且返回一个number类型. function genericDemo(data: number):

  • 详解Typescript里的This的使用方法

    this可以说是Javascript里最难理解的特性之一了,Typescript里的 this 似乎更加复杂了,Typescript里的 this 有三中场景,不同的场景都有不同意思. this 参数: 限制调用函数时的 this 类型 this 类型: 用于支持链式调用,尤其支持 class 继承的链式调用 ThisType: 用于构造复杂的 factory 函数 this 参数 由于 javascript 支持灵活的函数调用方式,不同的调用场景,this 的指向也有所不同 作为对象的方法调用

  • TypeScript泛型参数默认类型和新的strict编译选项

    概述 TypeScript 2.3 增加了对声明泛型参数默认类型的支持,允许为泛型类型中的类型参数指定默认类型. 接下来看看如何通过泛型参数默认将以下react组件从js(和jsX)迁移到 TypeScript (和TSX): class Greeting extends react.Component { render() { return <span>Hello, {this.props.name}!</span>; } } 为组件类创建类型定义 咱们先从为Component类

随机推荐