I'm trying to implement simple event dispatcher on TypeScript.
My custom types are declared like this:
events.d.ts
:
type AppEvent = { [key: string]: any }
type AppEventListener<T> = <T extends AppEvent>(event: T) => void;
When I pass any object in this function
public addListener<T extends AppEvent>(event: T, listener: AppEventListener<T>)
I get the following error:
Argument of type '(event: TestEvent) => void' is not assignable to parameter of type 'AppEventListener<typeof TestEvent>'.
Types of parameters 'event' and 'event' are inpatible.
Type 'T' is not assignable to type 'TestEvent'.
Property 'name' is missing in type 'AppEvent' but required in type 'TestEvent'.
So, this type AppEvent
cannot have any additional properties. How to make it work? Any help appreciated.
Full code example:
class EventDispatcher {
private listeners: Map<AppEvent, Array<AppEventListener<AppEvent>>>
constructor() {
this.listeners = new Map();
}
public dispatch<T extends AppEvent>(event: T): void {
const listeners = this.listeners.get(event.constructor);
if (listeners) {
listeners.forEach((callback) => {
callback(event);
});
}
}
public addListener<T extends AppEvent>(event: T, listener: AppEventListener<T>): void {
let listeners = this.listeners.get(event);
if (!listeners) {
listeners = [listener];
this.listeners.set(event, listeners);
} else {
listeners.push(listener);
}
}
}
class TestEvent {
public name: string = ''
}
const dispatcher = new EventDispatcher();
const listener = (event: TestEvent) => {
console.dir(event);
};
dispatcher.addListener(TestEvent, listener);
const event = new TestEvent('name');
dispatcher.dispatch(event);
I'm trying to implement simple event dispatcher on TypeScript.
My custom types are declared like this:
events.d.ts
:
type AppEvent = { [key: string]: any }
type AppEventListener<T> = <T extends AppEvent>(event: T) => void;
When I pass any object in this function
public addListener<T extends AppEvent>(event: T, listener: AppEventListener<T>)
I get the following error:
Argument of type '(event: TestEvent) => void' is not assignable to parameter of type 'AppEventListener<typeof TestEvent>'.
Types of parameters 'event' and 'event' are inpatible.
Type 'T' is not assignable to type 'TestEvent'.
Property 'name' is missing in type 'AppEvent' but required in type 'TestEvent'.
So, this type AppEvent
cannot have any additional properties. How to make it work? Any help appreciated.
Full code example:
class EventDispatcher {
private listeners: Map<AppEvent, Array<AppEventListener<AppEvent>>>
constructor() {
this.listeners = new Map();
}
public dispatch<T extends AppEvent>(event: T): void {
const listeners = this.listeners.get(event.constructor);
if (listeners) {
listeners.forEach((callback) => {
callback(event);
});
}
}
public addListener<T extends AppEvent>(event: T, listener: AppEventListener<T>): void {
let listeners = this.listeners.get(event);
if (!listeners) {
listeners = [listener];
this.listeners.set(event, listeners);
} else {
listeners.push(listener);
}
}
}
class TestEvent {
public name: string = ''
}
const dispatcher = new EventDispatcher();
const listener = (event: TestEvent) => {
console.dir(event);
};
dispatcher.addListener(TestEvent, listener);
const event = new TestEvent('name');
dispatcher.dispatch(event);
Share
Improve this question
asked Jul 6, 2020 at 23:39
Dmitry SolovovDmitry Solovov
4331 gold badge3 silver badges9 bronze badges
2
-
2
First of all,
type AppEventListener<T> = <T extends AppEvent>(event: T) => void;
isn't right. You have two differentT
's there. You probably want eithertype AppEventListener<T extends AppEvent> = (event: T) => void;
ortype AppEventListener = <T extends AppEvent>(event: T) => void;
. – Alex Wayne Commented Jul 7, 2020 at 0:27 -
Thank you. First variant is preferable then because it allows to require the same type for both arguments in
addListener
method – Dmitry Solovov Commented Jul 7, 2020 at 10:39
1 Answer
Reset to default 4It looks like you should make AppEventLister
a generic type referring to a non-generic function, like this:
type AppEventListener<T> = (event: T) => void;
You're saying "an AppEventListener<T>
will take an event of type T
". That's what you want, right?
Your original definition was a generic type referring to a generic function, with two different generic parameters of the same name, equivalent to
type AppEventListenerBad<T> = <U>(event: U) => void;
(the rename doesn't change the type; it just makes makes more clear what was happening with type parameter name shadowing) meaning "an AppEventListener<T>
will ignore T
entirely and take an event of any type U
that the caller wants to specify", which is unlikely what you mean. And that's where your error is ing from: (event: TestEvent) => void
is not assignable to <U>(event: U) => void
.
Once you do this, a lot of other issues are solved, but they might bring up other problems. A major one is that the standard library typings for Map
represent a very basic mapping from keys of type K
to values of type V
. It does not have a way to say that keys of certain subtypes of K
map to values of programmatically determined subtypes of V
. In your case you could make a new interface for the way you are using Map
:
interface AppEventListenerMap {
get<T>(x: T): Array<AppEventListener<T>> | undefined;
set<T>(x: T, val: Array<AppEventListener<T>>): void;
}
And then in EventDispatcher
you make the listeners
property a value of type AppEventListenerMap
:
class EventDispatcher {
private listeners: AppEventListenerMap;
constructor() {
this.listeners = new Map();
}
That will mostly make your code example work, except for the fact that you seem to be using event
as the key sometimes, and using event.constructor
as the key other times. That inconsistency seems wrong, and you probably need to decide exactly the type relationship to get it working properly.
Anyway, hopefully this helps; good luck!
Playground link to code