Reputation: 333
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 incompatible.
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);
Upvotes: 5
Views: 36422
Reputation: 330411
It 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 coming 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!
Upvotes: 4