andywer / typed-emitter

šŸ”© Type-safe event emitter interface for TypeScript
MIT License
268 stars 24 forks source link

Constraining the type of the events object could improve typing #22

Closed ehaynes99 closed 2 years ago

ehaynes99 commented 2 years ago

Currently, the generic type for Events is unconstrained. Example:

type MyEmitter = TypedEventEmitter<123>;

This results in a type that expands to:

type MyEmitter = {
    addListener: <E extends "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString">(event: E, listener: 123[E]) => MyEmitter;
    on: <E extends "toString" | ... 4 more ... | "toLocaleString">(event: E, listener: 123[E]) => MyEmitter;
    ... 12 more ...;
    setMaxListeners: (maxListeners: number) => MyEmitter;
}

This could be done like:

type EventMap = {
  [key: string]: (...args: any[]) => void;
};

export default interface TypedEventEmitter<Events extends EventMap> {
  addListener<E extends keyof Events>(event: E, listener: Events[E]): this;
  on<E extends keyof Events>(event: E, listener: Events[E]): this;
  once<E extends keyof Events>(event: E, listener: Events[E]): this;
  prependListener<E extends keyof Events>(event: E, listener: Events[E]): this;
  prependOnceListener<E extends keyof Events>(event: E, listener: Events[E]): this;

  off<E extends keyof Events>(event: E, listener: Events[E]): this;
  removeAllListeners<E extends keyof Events>(event?: E): this;
  removeListener<E extends keyof Events>(event: E, listener: Events[E]): this;

  emit<E extends keyof Events>(event: E, ...args: Arguments<Events[E]>): boolean;
  eventNames(): (keyof Events | string | symbol)[];
  rawListeners<E extends keyof Events>(event: E): Events[E][];
  listeners<E extends keyof Events>(event: E): Events[E][];
  listenerCount<E extends keyof Events>(event: E): number;

  getMaxListeners(): number;
  setMaxListeners(maxListeners: number): this;
}

In addition to preventing improper usage, this would allow stronger typing for rawListeners and listeners, which currently return untyped Function[].

andywer commented 2 years ago

Good point, indeed! Will check out #23.

Btw, why did you delete the section about using the Parameters<> util type? I found that to be very valid argument. In fact, I couldn't believe that this type has been around since TS 3.1 (!) and I didn't take notice! šŸ˜„

ehaynes99 commented 2 years ago

When I wrote that part, I didn't notice that the Arguments type is exported by the library, so it would be a breaking change to remove it. However unlikely, someone could be using:

import { Arguments } from 'typed-emitter';

type OddType = Arguments<5>; // equivalent to `type OddType = [5];`

I'm happy to open an issue/PR for that as well, but semver patterns would dictate a major version 2 for it. Since the additional constraints on Events would already ensure that it's only ever used with functions, I figured it would be better to minimize the impact.

andywer commented 2 years ago

Well, constraining the event listeners type is a breaking change, anyway, so I wouldn't care too much šŸ˜‰

ehaynes99 commented 2 years ago

Fair enough. I added another commit to #23