简单实现类型安全的、能触发 CustomEvent 的 EventTarget

我想写一个 TypeScript 类,这个类提供一系列的事件可供监听。为了实现类型安全,改进开发体验,我自己研究了一下,实现了一个可以以泛型输入所有可能的事件类型的 TypedEventTarget 类。

废话不多说,直接上代码。

typed-event-target.d.ts 文件内容:

export class TypedEventTarget<T> extends EventTarget {
    // 这个类型体操是我从 `lib.dom.d.ts` 抄的我会乱说(
    addEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget<T>, ev: TypedCustomEvent<K, T[K]>) => any,
        options?: boolean | AddEventListenerOptions,
    ): void;
    addEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | AddEventListenerOptions,
    ): void;
    removeEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget<T>, ev: TypedCustomEvent<K, T[K]>) => any,
        options?: boolean | EventListenerOptions,
    ): void;
    removeEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | EventListenerOptions,
    ): void;
    dispatchEvent<K extends keyof T>(event: TypedCustomEvent<K, T[K]>): void;
}

export class TypedCustomEvent<S, T> extends CustomEvent<T> {
    constructor(type: S, eventInitDict?: CustomEventInit<T> | undefined);
}

typed-event-target.js 文件内容:

// 这里我们小小的欺骗了一下 tsc。
// 在运行时下,这个 EventTarget 其实就是原来的 EventTarget,
// 而非从 EventTarget 继承出来的类。这样可以避免非必要的性能开销。
export const TypedEventTarget = EventTarget;
export const TypedCustomEvent = CustomEvent;

将这两个文件同时放在一个合适的目录下,就搞定了!

使用方法

我们想要写一个 Person 类,这个类有 nameChangeageChange 这两个自定义事件。那么,我们可以这么写:

import { TypedEventTarget, TypedCustomEvent } from "@/utils/typed-event-target"; // 请根据项目实际情况修改路径

// 创建 Person 类的事件表
export interface PersonEventMap {
    nameChange: string;
    ageChange: number;
}

export default class Person extends TypedEventTarget<PersonEventMap> {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        super();
        this.name = name;
        this.age = age;
    }

    setName(name: string) {
        this.name = name;
        // 在使用 dispatchEvent 时,如果类型不正确,会出现错误
        this.dispatchEvent<"nameChange">(
            new CustomEvent("nameChange", {
                detail: name,
            }),
        );
    }

    setAge(age: number) {
        this.age = age;
        this.dispatchEvent<"ageChange">(
            new CustomEvent("ageChange", {
                detail: age,
            }),
        );
    }
}

调用这个类时,我们绑定事件也会有正确的补全提示和类型检查。

本博文的上一个版本

我想写一个 TypeScript 类,这个类提供一系列的事件可供监听。为了实现类型安全,改进开发体验,我自己研究了一下,实现了一个可以以泛型输入所有可能的事件类型的 TypedEventTarget 类。

废话不多说,直接上代码。

typed-event-target.d.ts 文件内容:

export default class TypedEventTarget<T> extends EventTarget {
    // 这个类型体操是我从 `lib.dom.d.ts` 抄的我会乱说(
    addEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget, ev: T[K]) => any,
        options?: boolean | AddEventListenerOptions,
    ): void;
    addEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | AddEventListenerOptions,
    ): void;
    removeEventListener<K extends keyof T>(
        type: K,
        listener: (this: TypedEventTarget, ev: T[K]) => any,
        options?: boolean | EventListenerOptions,
    ): void;
    removeEventListener(
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | EventListenerOptions,
    ): void;
    dispatchEvent<K extends keyof T>(event: T[K]): void;
}

typed-event-target.js 文件内容:

// 这里我们小小的欺骗了一下 tsc。
// 在运行时下,这个 EventTarget 其实就是原来的 EventTarget,
// 而非从 EventTarget 继承出来的类。这样可以避免非必要的性能开销。
export default EventTarget;

将这两个文件同时放在一个合适的目录下,就搞定了!

使用方法

我们想要写一个 Person 类,这个类有 nameChangeageChange 这两个自定义事件。那么,我们可以这么写:

import TypedEventTarget from "@/utils/typed-event-target"; // 请根据项目实际情况修改路径

// 创建 Person 类的事件表
export interface PersonEventMap {
    nameChange: CustomEvent<string>;
    ageChange: CustomEvent<number>;
}

export default class Person extends TypedEventTarget<PersonEventMap> {
    name: string;
    age: number;
    
    constructor(name: string, age: number) {
        super();
        this.name = name;
        this.age = age;
    }
    
    setName(name: string) {
        this.name = name;
        // 在使用 dispatchEvent 时,如果类型不正确,会出现错误
        this.dispatchEvent<"nameChange">(
            new CustomEvent("nameChange", {
                detail: name,
            }),
        );
    }
        
    setAge(age: number) {
        this.age = age;
        this.dispatchEvent<"ageChange">(
            new CustomEvent("ageChange", {
                detail: age,
            }),
        );
    }
}

调用这个类时,我们绑定事件也会有正确的补全提示和类型检查:

类型提示
在调用 addEventListener 事件时,会提供准确的名称提示。
类型提示
调用事件的详情内容时,推导出来的类型也是准确的。