2

I'd like to implement a subscribe/publish class in TypeScript. The problem is that each event type has a different type for the data and I cannot figure it out how to do it in a statically typed manner. This is what I currently have:

type EventType = "A" | "B" | "C"

interface EventPublisher {  
    subscribe(eventType: EventType, callback: (data: any) => void);
    publish(eventType: EventType, data: any);
}

Is there a way to get rid of any and do it in a way so that when I instantiate an eventPublisher with a type, say X, the subscribe and publish methods behave as follows?

interface X {
    "A": number;
    "B": string;
}

const publisher: EventPublisher<X> = ...;
publisher.publish("A", 1); // OK!
publisher.publish("A", "blah"); // Error, expected number by got string

I can define the interface signature like this:

interface EventPublisher<U extends { [key in EventType]? : U[key] }>

but cannot figure it out how to relate the U[key] to the data type in methods.

Wickoo
  • 6,745
  • 5
  • 32
  • 45

1 Answers1

3

You need to add a generic type parameter for the key on the methods, and use a type query to relate the event type to the argument type.

type EventType = "A" | "B" | "C"

interface EventPublisher<T extends { [ P in EventType]? : any }> {  
    subscribe<E extends EventType>(eventType: E, callback: (data: T[E]) => void): void;
    publish<E extends EventType>(eventType: E, data: T[E]) : void;
}

interface X {
    "A": number;
    "B": string;
}

const publisher: EventPublisher<X> = ...;
publisher.publish("A", 1); // OK!
publisher.publish("A", "blah"); //error
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • @Titan Two questions about your solution. First, now I can for example add an event `publisher.publish("C", null);` although type X doesn't have `C` defined. I understand that this is due to the structural typing system of TS, but would be nice to restrict `E` to exactly keys of `T`. Second, if I want have an `undefined` type, say `interface X { "C" : undefined }`, is there a way to make the `data` parameter optional so that I can call `publisher.publish("C")`? would appreciate it if you share your thoughts on these two issues. – Wickoo May 17 '18 at 09:28
  • @Wickoo you can use `` instead, should work as expected, but I did not test. – Titian Cernicova-Dragomir May 17 '18 at 09:32
  • @Wickoo if the handlers have a different number of parameters, it gets a bit more complicated. I have a similar answer, but with arbitrary parameter count https://stackoverflow.com/questions/50369299/can-i-reuse-the-parameter-definition-of-a-function-in-typescript – Titian Cernicova-Dragomir May 17 '18 at 09:34
  • @Titan Thanks! the first one works. For the second one, need some thinking to figure out how it works :-) – Wickoo May 17 '18 at 09:38
  • @Wickoo yeah.. the other versions is a bit of type system abuse, but it works ;-). I would stick with this one though if your needs are not that complex.. – Titian Cernicova-Dragomir May 17 '18 at 09:40
  • @Titan There is another issue, let's say I add ` "D": boolean` to X, but don't add `"D"` as an eventType. Now `publisher.publish("D", false);` is accepted, while `"D"` is not a known event type. The problem is in `T extends { [ P in EventType]? : any }`. Can we somehow restrict `T` so that it doesn't have any properties not defined in `EventType`? – Wickoo May 17 '18 at 12:18
  • Generally the type constrain represents the minimal interface the type has to implement so you could have a `T` with more properties. We can add a restriction that any extra properties have type `never` and this should trigger an weeeo if we have any extra properties: `interface EventPublisher] : never }>` – Titian Cernicova-Dragomir May 17 '18 at 12:25