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
: `onRedrawand
headerClick```.
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.
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)