Reputation: 267
I've got a Message
interface that takes a generic argument T
and sets the value of T
to be the type of an internal property called body
:
interface Message<T> {
body: T
}
I also have an EventHandler
interface that describes a generic function that takes a Message
as it's sole parameter:
interface EventHandler<T> {
(event: Message<T>): void
}
Finally, I have an Integration
class that has a subscribe
method and a subscriptions
map that both look like the following:
class Integration {
public subscriptions: new Map<string, EventHandler>()
protected subscribe<T>(eventName: string, handler: EventHandler<T>) {
this.subscriptions.set(eventName, handler)
}
}
Ultimately, I want the user to be able to define their own Integration
instances like this:
interface MyEventProperties {
foo: string
}
class MyIntegration extends Integration {
constructor() {
super()
this.subscribe('My Event Name', this.myEventHandler)
}
myEventHandler(event: Message<MyEventProperties>) {
// do something
}
}
The problem is that this doesn't work. Because I don't know the generic type that ALL EventHandler
functions will be getting I can't define the second generic parameter when instantiating the Map
:
class Integration {
public subscriptions: new Map<string, EventHandler>()
// This errors with: Generic type 'EventHandler<T>' requires 1 type argument(s).
protected subscribe<T>(eventName: string, handler: EventHandler<T>) {
this.subscriptions.set(eventName, handler)
}
}
My question is, am I approaching this problem incorrectly or am I missing something more obvious?
Upvotes: 2
Views: 1415
Reputation: 249476
The simple solution is to use unknown
as the generic parameter in the map. The below solution will work:
interface Message<T> {
body: T
}
interface EventHandler<T> {
(event: Message<T>): void
}
class Integration {
public subscriptions = new Map<string, EventHandler<unknown>>()
protected subscribe<T>(eventName: string, handler: EventHandler<T>) {
this.subscriptions.set(eventName, handler)
}
public publishEvent<T>(eventName: string, msg: Message<T>) {
(this.subscriptions.get(eventName) as EventHandler<T>)(msg);
}
}
interface MyEventProperties { foo: string }
class MyIntegration extends Integration {
constructor() {
super()
this.subscribe('My Event Name', this.myEventHandler)
this.subscribe('My Event Name Mistaken', this.myEventHandler) // no relation between name and event
}
myEventHandler(event: Message<MyEventProperties>) {
// do something
}
}
The problem though is there is no relation between the event name and the type of the event. This may be a problem both when subscribing (as highlighted above) but also when emitting the event (the message body would not be checked against the expected message type)
I would suggest adding an mapping type between event type and name:
interface Message<T> {
body: T
}
interface EventHandler<T> {
(event: Message<T>): void
}
class Integration<TEvents> {
public subscriptions = new Map<PropertyKey, EventHandler<unknown>>()
protected subscribe<K extends keyof TEvents>(eventName: K, handler: EventHandler<TEvents[K]>) {
this.subscriptions.set(eventName, handler)
}
public publishEvent<K extends keyof TEvents>(eventName: K, msg: Message<TEvents[K]>) {
(this.subscriptions.get(eventName) as EventHandler<TEvents[K]>)(msg);
}
}
interface MyEventProperties { foo: string }
class MyIntegration extends Integration<{
'My Event Name': MyEventProperties
}> {
constructor() {
super()
this.subscribe('My Event Name', this.myEventHandler)
this.subscribe('My Event Name Mistaken', this.myEventHandler) // error now
this.subscribe('My Event Name', (e: Message<string>) => { }) // error, type of body not ok
}
myEventHandler(event: Message<MyEventProperties>) {
// do something
}
}
let m = new MyIntegration();
m.publishEvent("My Event Name", 0) // error body not ok
m.publishEvent("My Event Name M") // error mistaken name
m.publishEvent("My Event Name", {
body: { foo: ""}
}) // ok message and body correct
Upvotes: 3