From 84397cb0cd6265e0ee79adbf1607beff12ca9f16 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Thu, 14 Oct 2021 12:25:39 +0200 Subject: [PATCH] feat: add support for typed events The StrictEventEmitter class that was defined in the socket.io-client repository ([1]) is moved here, so we don't need to create an intermediary class (Socket > StrictEventEmitter > Emitter) to get the proper types. As an additional benefit, the final bundle size should be decreased. BREAKING CHANGE: we now use a named export instead of a default export ```js // before import Emitter from "@socket.io/component-emitter" // after import { Emitter } from "@socket.io/component-emitter" ``` [1]: https://github.com/socketio/socket.io-client/blob/a9e5b85580e8edca0b0fd2850c3741d3d86a96e2/lib/typed-events.ts --- index.d.ts | 191 ++++++++++++++++++++++++++++++++++++++++++++---- index.js | 12 ++- test/emitter.js | 2 +- 3 files changed, 182 insertions(+), 23 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5cae626..49a74e1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,18 +1,179 @@ -interface Emitter { - on(event: Event, listener: Function): Emitter; - once(event: Event, listener: Function): Emitter; - off(event?: Event, listener?: Function): Emitter; - emit(event: Event, ...args: any[]): Emitter; - listeners(event: Event): Function[]; - hasListeners(event: Event): boolean; - removeListener(event?: Event, listener?: Function): Emitter; - removeEventListener(event?: Event, listener?: Function): Emitter; - removeAllListeners(event?: Event): Emitter; +/** + * An events map is an interface that maps event names to their value, which + * represents the type of the `on` listener. + */ +export interface EventsMap { + [event: string]: any; } -declare const Emitter: { - (obj?: object): Emitter; - new (obj?: object): Emitter; -}; +/** + * The default events map, used if no EventsMap is given. Using this EventsMap + * is equivalent to accepting all event names, and any data. + */ +export interface DefaultEventsMap { + [event: string]: (...args: any[]) => void; +} + +/** + * Returns a union type containing all the keys of an event map. + */ +export type EventNames = keyof Map & (string | symbol); + +/** The tuple type representing the parameters of an event listener */ +export type EventParams< + Map extends EventsMap, + Ev extends EventNames + > = Parameters; + +/** + * The event names that are either in ReservedEvents or in UserEvents + */ +export type ReservedOrUserEventNames< + ReservedEventsMap extends EventsMap, + UserEvents extends EventsMap + > = EventNames | EventNames; + +/** + * Type of a listener of a user event or a reserved event. If `Ev` is in + * `ReservedEvents`, the reserved event listener is returned. + */ +export type ReservedOrUserListener< + ReservedEvents extends EventsMap, + UserEvents extends EventsMap, + Ev extends ReservedOrUserEventNames + > = FallbackToUntypedListener< + Ev extends EventNames + ? ReservedEvents[Ev] + : Ev extends EventNames + ? UserEvents[Ev] + : never + >; + +/** + * Returns an untyped listener type if `T` is `never`; otherwise, returns `T`. + * + * This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833. + * Needed because of https://github.com/microsoft/TypeScript/issues/41778 + */ +type FallbackToUntypedListener = [T] extends [never] + ? (...args: any[]) => void | Promise + : T; + +/** + * Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type + * parameters for mappings of event names to event data types, and strictly + * types method calls to the `EventEmitter` according to these event maps. + * + * @typeParam ListenEvents - `EventsMap` of user-defined events that can be + * listened to with `on` or `once` + * @typeParam EmitEvents - `EventsMap` of user-defined events that can be + * emitted with `emit` + * @typeParam ReservedEvents - `EventsMap` of reserved events, that can be + * emitted by socket.io with `emitReserved`, and can be listened to with + * `listen`. + */ +export class Emitter< + ListenEvents extends EventsMap, + EmitEvents extends EventsMap, + ReservedEvents extends EventsMap = {} + > { + /** + * Adds the `listener` function as an event listener for `ev`. + * + * @param ev Name of the event + * @param listener Callback function + */ + on>( + ev: Ev, + listener: ReservedOrUserListener + ): this; + + /** + * Adds a one-time `listener` function as an event listener for `ev`. + * + * @param ev Name of the event + * @param listener Callback function + */ + once>( + ev: Ev, + listener: ReservedOrUserListener + ): this; -export default Emitter; + /** + * Removes the `listener` function as an event listener for `ev`. + * + * @param ev Name of the event + * @param listener Callback function + */ + off>( + ev?: Ev, + listener?: ReservedOrUserListener + ): this; + + /** + * Emits an event. + * + * @param ev Name of the event + * @param args Values to send to listeners of this event + */ + emit>( + ev: Ev, + ...args: EventParams + ): this; + + /** + * Emits a reserved event. + * + * This method is `protected`, so that only a class extending + * `StrictEventEmitter` can emit its own reserved events. + * + * @param ev Reserved event name + * @param args Arguments to emit along with the event + */ + protected emitReserved>( + ev: Ev, + ...args: EventParams + ): this; + + /** + * Returns the listeners listening to an event. + * + * @param event Event name + * @returns Array of listeners subscribed to `event` + */ + listeners>( + event: Ev + ): ReservedOrUserListener[]; + + /** + * Returns true if there is a listener for this event. + * + * @param event Event name + * @returns boolean + */ + hasListeners< + Ev extends ReservedOrUserEventNames + >(event: Ev): boolean; + + /** + * Removes the `listener` function as an event listener for `ev`. + * + * @param ev Name of the event + * @param listener Callback function + */ + removeListener< + Ev extends ReservedOrUserEventNames + >( + ev?: Ev, + listener?: ReservedOrUserListener + ): this; + + /** + * Removes all `listener` function as an event listener for `ev`. + * + * @param ev Name of the event + */ + removeAllListeners< + Ev extends ReservedOrUserEventNames + >(ev?: Ev): this; +} diff --git a/index.js b/index.js index 6c1dfc0..e0d5497 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,7 @@ * Expose `Emitter`. */ -if (typeof module !== 'undefined') { - module.exports = Emitter; -} +exports.Emitter = Emitter; /** * Initialize a new `Emitter`. @@ -15,10 +13,7 @@ if (typeof module !== 'undefined') { function Emitter(obj) { if (obj) return mixin(obj); -}; - -// allow default import -Emitter.default = Emitter; +} /** * Mixin the emitter properties. @@ -152,6 +147,9 @@ Emitter.prototype.emit = function(event){ return this; }; +// alias used for reserved events (protected method) +Emitter.prototype.emitReserved = Emitter.prototype.emit; + /** * Return array of callbacks for `event`. * diff --git a/test/emitter.js b/test/emitter.js index a1f1dda..6a6148e 100644 --- a/test/emitter.js +++ b/test/emitter.js @@ -1,5 +1,5 @@ -var Emitter = require('..'); +var { Emitter } = require('..'); function Custom() { Emitter.call(this)