Skip to content

Instantly share code, notes, and snippets.

@lilBunnyRabbit
Last active November 28, 2024 09:50
Show Gist options
  • Select an option

  • Save lilBunnyRabbit/5c4370375c4974220f20c8b7a392de91 to your computer and use it in GitHub Desktop.

Select an option

Save lilBunnyRabbit/5c4370375c4974220f20c8b7a392de91 to your computer and use it in GitHub Desktop.
Event Handling in Typescript

TypeScript Event Handling

Important

The event handling functionality previously available in this gist has been officially integrated into the @lilbunnyrabbit/event-emitter package. For continued support and access to the latest features, please refer to the package.

The EventEmitter class provides a powerful and flexible mechanism for managing and handling custom events, similar in functionality to the standard EventTarget interface found in web APIs. This class allows for easy creation of event-driven architectures in TypeScript applications, enabling objects to publish events to which other parts of the application can subscribe. It's particularly beneficial in scenarios where you need to implement custom event logic or when working outside of environments where EventTarget is not available or suitable. With EventEmitter, you can define event types, emit events, and dynamically attach or detach event listeners, all within a type-safe and intuitive API.

Usage

Creating an EventEmitter

Start by creating an instance of the EventEmitter class. You can define the types of events it will handle using a TypeScript interface.

import { EventEmitter } from "./EventEmitter";

interface MyEvents {
  data: string;
  loaded: void;
  error: Error;
}

const emitter = new EventEmitter<MyEvents>();

Registering Event Listeners

To listen for events, use the on method. Define the event type and provide a callback function that will be executed when the event is emitted.

emitter.on("data", (data: string) => {
  console.log("Data", data);
});

emitter.on("loaded", function () {
  console.log(
    "Emitter loaded",
    this // EventEmitter<MyEvents>
  );
});

emitter.on("error", (error: Error) => {
  console.error(`Error: ${error.message}`);
});

Removing Event Listeners

You can remove a specific event listener by using the off method, specifying the event type and the listener to remove.

const onError = (error: Error) => console.error(error);

emitter.on("error", onError);

// ...

emitter.off("error", onError);

Emitting Events

Use the emit method to trigger an event. This will invoke all registered listeners for that event type.

emitter.emit("data", "Sample data");
emitter.emit("loaded");
emitter.emit("error", new Error("Oh no!"));

Extending EventEmitter

For more specialized use cases, you can extend the EventEmitter class. This allows you to create a custom event emitter with additional methods or properties tailored to specific needs. When extending, you can still take full advantage of the type safety and event handling features of the base class.

import { EventEmitter } from './EventEmitter';

interface MyServiceEvents {
  dataLoaded: string;
  error: Error;
}

// Extending the EventEmitter class
class MyService extends EventEmitter<MyServiceEvents> {
  // Custom method
  loadData() {
    try {
      // Load data and emit a `dataLoaded` event
      const data = "Sample Data";
      this.emit("dataLoaded", data);
    } catch (error) {
      // Emit an `error` event
      this.emit("error", error);
    }
  }
}

const service = new MyService();

service.on("dataLoaded", function (data) {
  console.log(
    `Data loaded: ${data}`,
    this // MyService
  );
});

service.on("error", (error) => console.error(`Error: ${error.message}`));

// Using the custom method
myEmitter.loadData();

In this example, MyService extends the EventEmitter class, adding a custom method loadData. This method demonstrates how to emit dataLoaded and error events, integrating the event-emitting functionality into a more complex operation.

API

EventEmitter

on(type, listener): this

Registers an event listener for a specific event type. The on method allows you to define the event type and provide a callback function that will be executed when the event is emitted.

off(type, listener): this

Removes a registered event listener for a specific event type. The off method is used to specify the event type and the listener to remove. This is useful for cleaning up listeners when they are no longer needed or when an object is being disposed of.

emit(type, [data]): this

Note
For cases where EventEmitter is used as a base class and you wish to prevent external invocation of the emit method, set it to protected. This allows subclasses to emit events while preventing access from outside the class hierarchy.

Emits an event of a specific type, calling all registered listeners for that event type. The emit method is used to trigger the event and optionally pass data to the event listeners. It's a crucial part of the event-driven architecture, enabling dynamic and responsive applications.

Conclusion

The custom EventEmitter class in TypeScript provides a flexible and type-safe foundation for event-driven programming. By extending this class, you can create tailored solutions that fit the unique requirements of your application, while maintaining clean and maintainable code.

/**
* Event listener function.
* The listener function's context (`this`) is bound to the EventEmitter instance.
*
* @template TEvents - Object type representing the event types with associated data types.
* @template TData - Data type that the event listener will receive.
* @template TEmitter - Type of the EventEmitter instance.
*/
export type EventListener<
TEvents extends Record<string | number | symbol, unknown>,
TData,
TEmitter extends EventEmitter<TEvents>
> = (this: TEmitter, ...data: TData extends void ? [] : [data: TData]) => void | Promise<void>;
/**
* Event emitter class that can emit and listen to typed events.
*
* @template TEvents - Object type representing the event types with associated data types.
*/
export class EventEmitter<TEvents extends Record<string, unknown>> {
private _events: Partial<{
[TType in keyof TEvents]: Array<EventListener<TEvents, TEvents[TType], this>>;
}> = {};
/**
* Registers event listener for a specific event type.
*
* @template TType - Event type.
* @param type - The event type to listen for.
* @param listener - Event listener function to register.
* @returns - EventEmitter instance, for method chaining.
*/
public on<TType extends keyof TEvents>(type: TType, listener: EventListener<TEvents, TEvents[TType], this>): this {
if (!(type in this._events)) {
this._events[type] = [];
}
this._events[type]!.push(listener);
return this;
}
/**
* Removes a registered event listener for a specific event type.
*
* @template TType - Event type.
* @param type - The event type to remove the listener from.
* @param listener - Event listener function to remove.
* @return - EventEmitter instance, for method chaining.
*/
public off<TType extends keyof TEvents>(type: TType, listener: EventListener<TEvents, TEvents[TType], this>): this {
if (this._events[type] !== undefined) {
this._events[type] = this._events[type]!.filter((savedListener) => savedListener !== listener);
}
return this;
}
/**
* Emits an event of a specific type, calling all registered listeners for that event type.
*
* @template TType - Event type.
* @param type - The event type to emit.
* @param data - Data associated with the event.
* @return - EventEmitter instance, for method chaining.
*/
public emit<TType extends keyof TEvents>(
type: TType,
...data: TEvents[TType] extends void ? [] : [data: TEvents[TType]]
): this {
this._events[type]?.forEach((listener) => listener.apply(this, data));
return this;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment