ccnixon
ccnixon

Reputation: 267

Map of generic functions in Typescript

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

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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

Related Questions