2.0.0 • Published 4 years ago

event-as-a-property v2.0.0

Weekly downloads
1
License
MIT
Repository
github
Last release
4 years ago

A strongly typed event-as-a-property wrapper for Node's EventEmitter.

Synopsis

All methods delegate to the EventEmitter passed in the constructor. Their semantics are identical, except the event name is implicit, and the return type is void.

export default class Event<TArgs extends any[] = []> {
  readonly name: string | symbol;

  constructor(emitter: EventEmitter, name?: string | symbol = Symbol()) {}

  /** has to be the same `EventEmitter` from which it was constructed */
  emit(emitter: EventEmitter, ...args: TArgs): void;

  addListener(listener: (...args: TArgs) => void): void;
  on(listener: (...args: TArgs) => void): void;

  once(listener: (...args: TArgs) => void): void;

  removeListener(listener: (...args: TArgs) => void): void;
  off(listener: (...args: TArgs) => void): void;

  removeAllListeners(): void;

  prependListener(listener: (...args: TArgs) => void): void;
  prependOnceListener(listener: (...args: TArgs) => void): void;

  promise(): Promise<TArgs>;
}

Usage

import { EventEmitter } from "events";
import Event from "event-as-a-property";

class Publisher {
  /** create an `EventEmitter` for the event source */
  private _emitter = new EventEmitter();

  /** declare the event as a property on the event source */
  onPublish = new Event<[date: Date, text: string]>(this.emitter);

  publish(date: Date, text: string): void {
    /** emit the event using the `EventEmitter` */
    this.emitter.emit(this.onPublish.name, date, text);

    /** or use a typed wrapper */
    this.onPublish.emit(this._emitter, date, text);
  }
}

const publisher = new Publisher();

/** subscribe to an event using the event propery */
publisher.onPublish.addListener((date, text) => console.log(date, text));

/** or add a listener that fires only once */
publisher.onPublish.once((date, text) => console.log(date, text));

/** or await the next event */
const [date, text] = await publisher.onPublish.promise();

Why?

self-documenting code

/**
 * What events does `Bad` have?
 * You need to either document it textually,
 * or provide _seven_ method overloads per event.
 */
interface Bad extends EventEmitter {
  addListener(event: "exit", listener: (status: number) => void): void;
  on(event: "exit", listener: (status: number) => void): void;
  once(event: "exit", listener: (status: number) => void): void;
  prependListener(event: "exit", listener: (status: number) => void): void;
  prependOnceListener(event: "exit", listener: (status: number) => void): void;
  removeListener(event: "exit", listener: (status: number) => void): void;
  off(event: "exit", listener: (status: number) => void): void;
}

/**
 * `Good` has an `exit` event with a `status` parameter that is a `number`.
 */
interface Good {
  readonly exit: Event<[status: number]>;
}

separation of concerns

/**
 * Why should clients be able to emit events?
 */
let bad: Bad;
/* ... */
bad.addListener("exit", console.log);
bad.emit("exit", 0);

/**
 * Have to have the right EventEmitter to emit events.
 */
let good: Good;
/* ... */
good.exit.addListener(console.log);
good.exit.emit(new EventEmitter(), 0); // error: wrong EventEmitter

easy to navigate and refactor

/**
 * To rename the event, you have to somehow find all subscribers
 * and change the `"exit"` literal in each of them
 */
let bad: Bad;
/* ... */
bad.addListener("exit", console.log);

/**
 * To rename the event, you just have to rename the property with an IDE.
 * To find subscribers, you can just `Find All Usages` with an IDE.
 */
let good: Good;
/* ... */
good.exit.addListener(console.log);

easier to use with documentation generators

/**
 * You can use JSDoc.
 * @event exit
 */
interface Bad extends EventEmitter {
  /**
   * Whoops! Event has been renamed, but the docs are not updated.
   */
  addListener(event: "stop", listener: (status: number) => void): void;
}

interface Good {
  /**
   * Event docs are inseparable from the event property.
   */
  readonly exit: Event<[status: number]>;
}

easy to migrate from EventEmitter

/**
 * @event start
 * @event exit
 */
class Bad extends EventEmitter {
  /* ... */
}

/**
 * First, add the event properties.
 * Then, deprecate the methods inherited from `EventEmitter`
 */
class Better extends EventEmitter {
  readonly start = new Event<[]>(this, "start");
  readonly exit = new Event<[status: number]>(this, "exit");
}

/**
 * Finally, move `EventEmitter` to an implementation detail.
 */
class Best {
  private readonly _emitter = new EventEmitter();

  readonly start = new Event<[]>(this._emitter);
  readonly exit = new Event<[status: number]>(this._emitter);
}