typescriptgenericseventemitterextending-classes

Extending event emitter with class


I have problem to correctly create EventEmitter with Listener instances inside, and use it with extending. I want to create Event Emitter that can be extended by additional events by child class if there is any extending. Let's look at my code, and below there is more description.

EventEmitter.ts

export type ValidReturnValue = void | Record<string, any>;
export type ValidEventTypes = Record<string, (...args: any[]) => ValidReturnValue>;

class Listener<T extends (...args: any) => ValidReturnValue> {
    constructor(private readonly callback: T) {
        // Do something there
    }

    run(...args: Parameters<T>): ValidReturnValue {
        return this.callback(args);
    }
}

export class EventEmitter<T extends ValidEventTypes> {
    private readonly listeners = new Map<keyof T, Array<Listener<T[keyof T]>>>();

    constructor(private readonly context: any) {}

    on<K extends keyof T>(eventName: K, callback: T[K]): Listener<T[K]> {
        const listener = new Listener(callback);
        return listener;
    }
}

Parent.ts

import {type TypeId} from '../Component';
import {EventEmitter} from './EventsEmitter';

export declare type ParentEvents = {
    onRedraw: (dom: HTMLElement) => void;
    headerClick: (id: TypeId, e: MouseEvent) => void;
};

export class Parent {
    public events = new EventEmitter<ParentEvents>(this);

    constructor() {
        this.events.on('onRedraw', () => {
            // Do something
        });
    }
}

Child.ts

import {type TypeId} from '../Component';
import {EventEmitter} from './EventsEmitter';
import {Parent, type ParentEvents} from './Parent';

declare type ChildEvents = {
    click: (id: TypeId, e: MouseEvent) => void;
};

export class Child extends Parent {
    public events = new EventEmitter<ChildEvents & ParentEvents>(this);

    constructor() {
        super();
        this.events.on('click', () => {
            console.log(1);
        });
    }
}

In the final I would like to have state, that Child instance has three possible events to use; click and inherited from Parent: `onRedrawandheaderClick```.

And difference is made by line in EventsEmitter where field listeners is defined. If I remove its declaration, there is no errors by TypeScript, but when it exists, I have error in Child class where public field events is declared. Below there is text of error:

roperty 'events' in type 'Child' is not assignable to the same property in base type 'Parent'.
  Type 'EventEmitter<ChildEvents & ParentEvents>' is not assignable to type 'EventEmitter<ParentEvents>'.
    Types of property 'listeners' are incompatible.
      Type 'Map<"click" | keyof ParentEvents, Listener<((id: TypeId, e: MouseEvent) => void) | ((dom: HTMLElement) => void) | ((id: TypeId, e: MouseEvent) => void)>[]>' is not assignable to type 'Map<keyof ParentEvents, Listener<((dom: HTMLElement) => void) | ((id: TypeId, e: MouseEvent) => void)>[]>'.
        Type '"click" | keyof ParentEvents' is not assignable to type 'keyof ParentEvents'.
          Type '"click"' is not assignable to type 'keyof ParentEvents'.ts(2416)

I tried in many ways to fix it but eventually I am always at the same place with that error or similar. But I am not expert about TypeScript, and I hope that you are able to tell me what's wrong there is or please give me any advices how could I find proper solution.


Solution

  • Your problem happens because unlike Record, a broader Map does not extends lesser Map:

    let o2: {a:1, b:2} = {a:1, b: 2};
    let o1: {a: 1} = o2;
    
    let m2 = new Map<'a'|'b', 1|2>;
    let m1: Map<'a', 1> = m2; 
    //  ^! Type 'Map<"a" | "b", 1 | 2>' is not assignable to type 'Map<"a", 1>'.
    

    The optimal solution would be untype

        private readonly listeners = new Map<keyof T, Array<Listener<T[keyof T]>>>();
    

    to

        private readonly listeners = new Map();
    

    (as it's private anyway)

    Alternative solution is to cast EventEmitter as

    export class Child extends Parent {
        public events = new EventEmitter<ChildEvents & ParentEvents>(this) as
                            EventEmitter<ChildEvents> & EventEmitter<ParentEvents>;
    }
    

    If that would be a Record rather then a Map this cast would be implitic (or rather not needed)