Skip to content

Typed Events Map #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
CMCDragonkai opened this issue Sep 15, 2023 · 2 comments
Open

Typed Events Map #12

CMCDragonkai opened this issue Sep 15, 2023 · 2 comments
Labels
development Standard development r&d:polykey:supporting activity Supporting core activity

Comments

@CMCDragonkai
Copy link
Member

CMCDragonkai commented Sep 15, 2023

Specification

Sometimes we get some weird type error about the fact that we cannot specify a more specific event type in our event handlers because event target isn't typed and thus allows any event to be dispatched to any event name.

To get around this we can extend our Evented interface to actually be typesafe and to receive an optional EventMap that would allow downstream classes to specify a typed map of events that is being used.

This can be propagated to the js-async-init too, and it would be possible to do things like:

interface X extends StartStop<EventMap, StartReturn, StopReturn> {}
@StartStop({
  eventStart,
  eventStarted,
  eventStop,
  eventStopped
})
class X {
}

It would be a breaking change on the types, but we don't really use the StartReturn and StopReturn types much. And actually all the generic types would be optional here. The EventMap can be first since it is just likely to be used more.

Anyway this is what we would do to Evented to achieve this:

type EventListenerOrEventListenerObject<T extends Event> = ((evt: T) => void) | { handleEvent(evt: T): void };

class TypedEventTarget<E extends Record<string, Event> = Record<string, Event>> {
  private target: EventTarget = new EventTarget();

  // General case: any string maps to an Event
  addEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject<Event>,
    options?: boolean | AddEventListenerOptions
  ): void;

  // Specific case: string is a key in E
  addEventListener<K extends keyof E>(
    type: K,
    listener: EventListenerOrEventListenerObject<E[K]>,
    options?: boolean | AddEventListenerOptions
  ): void;

  // Implementation for addEventListener
  addEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject<Event>,
    options?: boolean | AddEventListenerOptions
  ) {
    this.target.addEventListener(type, listener as EventListenerOrEventListenerObject<Event>, options);
  }

  // General case: any string maps to an Event
  removeEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject<Event>,
    options?: boolean | EventListenerOptions
  ): void;

  // Specific case: string is a key in E
  removeEventListener<K extends keyof E>(
    type: K,
    listener: EventListenerOrEventListenerObject<E[K]>,
    options?: boolean | EventListenerOptions
  ): void;

  // Implementation for removeEventListener
  removeEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject<Event>,
    options?: boolean | EventListenerOptions
  ) {
    this.target.removeEventListener(type, listener as EventListenerOrEventListenerObject<Event>, options);
  }

  dispatchEvent<K extends keyof E>(type: K, event: E[K]) {
    this.target.dispatchEvent(event as Event);
  }
}

The above is pseudo code generated by chatgpt. What's cool is that it preserves the ability to not bother specifying specific types if you don't want to. You can still get the original behaviour of event map not caring about the type of the event. When you do care, you give it a much more specific type.

The relevant type map can then look like this:

interface MyEvents {
  'myEvent': MyEvent;
  'anotherEvent': AnotherEvent;
}

One might be careful that EventQUICConnectionStart.name is of type string and not the literal type of EventQUICConnectionStart.name. So I'm not sure how TS will end up inferring it or not. If not, then you have to do addEventListener<'EventQUICConnectionStart'>(...).

A little more boilerplate.

Additional Context

Tasks

  1. Integrate the above to Evented
  2. Test that default behaviour still works
  3. Test ways of having succinct was of expressing this
  4. Integrate this to js-async-init before enabling this feature
  5. Test that we can directly add typed events without bothering with @ts-ignore.
@CMCDragonkai CMCDragonkai added the development Standard development label Sep 15, 2023
@CMCDragonkai
Copy link
Member Author

This is what it looks like when we have to add specific event types:

  quicServer.addEventListener(
    events.EventQUICServerConnection.name,
    // @ts-ignore
    (evt: events.EventQUICServerConnection) => {
      const connection = evt.detail;
      connection.addEventListener(
        events.EventQUICConnectionStream.name,
        // @ts-ignore
        async (evt: events.EventQUICConnectionStream) => {
          const stream = evt.detail;
          // Graceful close of writable
          process.stderr.write('>>>>>>>>> HANDLING THE QUIC SERVER STREAM\n');
          await stream.writable.close();
          // Consume until graceful close of readable
          for await (const _ of stream.readable) {
            // Do nothing, only consume
          }
          process.stderr.write('<<<<<<<< HANDLED THE QUIC SERVER STREAM\n');
        }
      );
    }
  );

It doesn't happen when those event handlers are defined elsewhere, but this can be annoying. If the Class.name could be made into literal types would be even easier to do...

@CMCDragonkai
Copy link
Member Author

This would be useful microsoft/TypeScript#1579.

@CMCDragonkai CMCDragonkai changed the title Typed Events Typed Events Map Sep 15, 2023
@CMCDragonkai CMCDragonkai added the r&d:polykey:supporting activity Supporting core activity label Aug 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
development Standard development r&d:polykey:supporting activity Supporting core activity
Development

No branches or pull requests

1 participant