1

I'm trying to take the following code for an event dispatcher, and make the types used (classes and interfaces) more specified.

  • Note that I'm generally interested in understanding why my usage here with types/classes/interfaces/generic types here doesn't work, and less interested in events per se.

Original code:

type Handler<E> = (event: E) => void;

class EventDispatcher<E> { 
    private handlers: Handler<E>[] = [];
    fire(event: E) { 
        for (let h of this.handlers)
            h(event);
    }
    register(handler: Handler<E>) { 
        this.handlers.push(handler);
    }
} 

What I'd like to achieve:

  • Use Typescript's CustomEvent<any> instead of E.

  • Be able to define specific custom events that extends CustomEvent<any>, for example:

    export class FormClearedEvent extends CustomEvent<any> {
       constructor() {
         super("formCleared");
       }
    }
    
  • Export those specific custom event, together with the handler and the event dispacther

First attempt, doesn't compile:

export type Handler<CustomEvent<any>> = (event: CustomEvent<any>) => void;    // <-- doesn't compile
// Also, using CustomEvent<any> as the type parameter for the dispacther and its methods, doesn't compile as well. 

Second attempt, makes me duplicate SpecificEvent extends CustomEvent<any>> all over the place, and doesn't compile:

export type Handler<SpecificEvent extends CustomEvent<any>> = (event: SpecificEvent) => void;
export class EventDispatcher<SpecificEvent extends CustomEvent<any>> {
    private handlers: Handler<SpecificEvent extends CustomEvent<any>>[] = []; // <-- doesn't compile
    fire(event: SpecificEvent extends CustomEvent<any>) {                     // <-- doesn't compile
        for (let handler of this.handlers) {
            handler(event);
        }
    }
    register(handler: Handler<SpecificEvent extends CustomEvent<any>>) {      // <-- doesn't compile
        this.handlers.push(handler);
    }
}

// specific events
export class FormClearedEvent extends CustomEvent<any> {
   constructor() {
     super("formCleared");
   }
}

Third attempt, works, but now I have two classes which are really the same one (CustomEvent<any> and SpecificEvent):

// specific events
class SpecificEvent extends CustomEvent<any> { }
export class FormClearedEvent extends CustomEvent<any> {
   constructor() {
     super("formCleared");
   }
}

// SpecificEvent handler
export type Handler<SpecificEvent> = (event: SpecificEvent) => void;

// SpecificEvent dispatcher
export class EventDispatcher<SpecificEvent> {
    private handlers: Handler<SpecificEvent>[] = [];
    fire(event: SpecificEvent) {
        for (let handler of this.handlers) {
            handler(event);
        }
    }
    register(handler: Handler<SpecificEvent>) {
        this.handlers.push(handler);
    }
}

My Question:

  1. Why my first attempt doesn't compile?
  2. Why some of the code in my second attempt doesn't compile?
  3. Can I do better than the third way?

1 Answer 1

0

Counter-intuitively, this might make more sense if we use less meaningful names.

In the first case, typescript is looking for something like this:

export type Handler<T> = (event: T) => void;

We're trying to use CustomEvent<any> as the type parameter:

export type Handler<CustomEvent<any>> = (event: CustomEvent<any>) => void; 

-- which isn't a valid name. That explains why our second attempt gets past that; we've given it a valid name, SpecificEvent, and constrained it to extend CustomEvent:

export type Handler<SpecificEvent extends CustomEvent<any>> = (event: SpecificEvent) => void;

Similarly, now that we have a generic type named SpecificEvent in our class declaration:

export class EventDispatcher<SpecificEvent extends CustomEvent<any>> 

-- typescript thinks that the subsequent extends are going to be conditional types, and expects a question mark.

It looks like you can do something along these lines:

export type Handler<E extends CustomEvent> = (event: E) => void;

export class EventDispatcher<E extends CustomEvent> {
  private handlers: Handler<E>[] = [];
  fire(event: E) {
    for (let handler of this.handlers) {
      handler(event);
    }
  }
  register(handler: Handler<E>) {
    this.handlers.push(handler);
  }
}
Sign up to request clarification or add additional context in comments.

1 Comment

(1) But why CustomEvent<any> is not valid in place of T? I thought T means anything, but it doesn't; specifically, it doesn't mean generic type, right? Can you link to some docs that specify that? (2) Why Typescript thinks it's a conditional type inside class EventDispatcher definition, but not inside Handler definition?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.