How the Event Emitter Pattern Works in LINEJS: TypedEventEmitter Explained

LINEJS implements a strictly-typed event emitter using TypeScript generics that enforces compile-time validation of event names and listener arguments through the TypedEventEmitter class in packages/linejs/base/core/typed-event-emitter/index.ts.

The evex-dev/linejs repository provides a LINE messenger SDK built on a custom typed event emitter that eliminates runtime type errors. This pattern powers real-time message handling across the Client, SquareChat, and Group classes by binding event names to specific function signatures at compile time.

Core Implementation of the Event Emitter Pattern

The TypedEventEmitter class serves as the foundation for all event-driven functionality in LINEJS. Located in packages/linejs/base/core/typed-event-emitter/index.ts, this generic class accepts two type parameters: T, which maps event names to their listener function signatures, and E, which constrains the allowable event names to the keys of T.

Generic Type Architecture

The class signature export class TypedEventEmitter<T extends Record<string, (...args: any[]) => any>, E extends keyof T = keyof T> ensures that every event name passed to the emitter must exist as a key in the provided event map. This architecture prevents typos in event names and guarantees that listeners receive arguments of the correct type without runtime checks.

Listener Storage and Validation

Internally, the emitter stores registered callbacks in a Map<E, T[E][]> structure (see line 9 of the source). When adding listeners via the on() method, the implementation validates that each listener is a function (lines 17-19), throwing immediately if non-callable values are passed.

Event Emitter Methods and Signatures

The LINEJS event emitter pattern exposes four primary methods that mirror the Node.js EventEmitter API while maintaining strict type safety.

Registering Listeners with on()

The on() method accepts an event name and one or more listener functions. Its signature on<E2 extends E>(event: E2, ...listeners: T[E2][]): this uses a generic method parameter E2 to ensure that listeners match the exact signature defined in the event map for that specific event.

Removing Listeners with off()

Correspondingly, the off() method uses the signature off<E2 extends E>(event: E2, ...listeners: T[E2][]): this to remove specific previously-registered listeners. Both methods return this to enable method chaining.

Emitting Events with emit()

The emit() method synchronously invokes all registered listeners with the signature emit<E2 extends E>(event: E2, ...args: Parameters<T[E2]>): this. TypeScript extracts the parameter types from the event map using Parameters<T[E2]>, ensuring that emitted arguments match the expected listener signature exactly.

One-Shot Promises with waitFor()

For asynchronous workflows, the waitFor() method returns a Promise that resolves the first time a specified event fires. Its signature waitFor<E2 extends E, P = Parameters<T[E2]>>(event: E2): Promise<P> captures the event arguments as the promise resolution value. After resolution, the temporary listener automatically deregisters (lines 55-62), preventing memory leaks.

Real-World Usage in LINEJS Components

The event emitter pattern permeates the LINEJS architecture, from the high-level Client API to specific feature implementations.

Client Events and Message Handling

The Client class in packages/linejs/client/client.ts extends TypedEventEmitter<ClientEvents>, exposing type-safe hooks for "message", "square:message", "ready", and "event". When internal polling logic receives a new LINE message, it constructs a high-level TalkMessage or SquareMessage object and calls this.emit("message", message), triggering all registered listeners synchronously.

Feature-Specific Emitters

Individual feature classes like SquareChat, User, and Group each extend TypedEventEmitter with their own event maps. For example, SquareChat might define events like "member:joined" and "member:left", while User tracks "update:profile" changes. This granularity ensures that event payloads remain strongly typed across different domains of the LINE protocol.

Base Client Infrastructure

Low-level connection events—such as "login" and "update:profile"—are defined in packages/linejs/base/core/utils/events.ts and exposed through the BaseClient class (referenced in packages/linejs/base/core/mod.ts). This separation allows the base layer to handle protocol-level state changes while the high-level Client manages user-facing message events.

Code Examples

Basic TypedEventEmitter Usage

import { TypedEventEmitter } from "https://deno.land/x/linejs/packages/linejs/base/core/typed-event-emitter/index.ts";

type MyEvents = {
  greet: (name: string) => void;
  data: (payload: { id: number; value: string }) => void;
};

const emitter = new TypedEventEmitter<MyEvents>();

// TypeScript validates these signatures at compile time
emitter.on("greet", (name) => console.log(`Hello, ${name}!`));
emitter.on("data", (p) => console.log(`Received ${p.id}: ${p.value}`));

// Emitting with correct argument types
emitter.emit("greet", "Alice");
emitter.emit("data", { id: 42, value: "answer" });

// Promise-based one-shot listener
const [name] = await emitter.waitFor("greet");

Listening to LINEJS Client Events

import { Client } from "https://deno.land/x/linejs/client.ts";

const client = new Client();

// Register typed handlers for specific LINE events
client.on("message", (msg) => {
  console.log("New talk message:", msg.text);
});

client.on("square:message", (msg) => {
  console.log("Square notification:", msg.content);
});

// Wait for ready state before starting polling
await client.waitFor("ready");
client.listen({ talk: true, square: true });

Creating Custom Feature Classes

import { TypedEventEmitter } from "../base/core/typed-event-emitter/index.ts";

type SquareChatEvents = {
  "member:joined": (mid: string, displayName: string) => void;
  "member:left": (mid: string) => void;
};

export class SquareChat extends TypedEventEmitter<SquareChatEvents> {
  handleMembershipChange(rawEvent: any) {
    // Internal logic validates and emits type-safe events
    if (rawEvent.type === "join") {
      this.emit("member:joined", rawEvent.mid, rawEvent.name);
    } else {
      this.emit("member:left", rawEvent.mid);
    }
  }
}

Summary

  • The event emitter pattern in LINEJS centers on the TypedEventEmitter class located in packages/linejs/base/core/typed-event-emitter/index.ts.
  • Generic type parameters T and E enforce compile-time validation of event names and listener signatures, preventing runtime type errors.
  • The emitter provides on, off, emit, and waitFor methods, with waitFor offering automatic cleanup of one-shot promise listeners.
  • Real-time messaging relies on this pattern through the Client class (packages/linejs/client/client.ts) and specialized feature classes like SquareChat and User.
  • Event definitions in packages/linejs/base/core/utils/events.ts separate protocol-level events from high-level message handling.

Frequently Asked Questions

How does LINEJS ensure type safety in its event emitter?

LINEJS enforces type safety through TypeScript generics in the TypedEventEmitter class. The T parameter maps event names to function signatures, while E constrains valid event keys. When calling on() or emit(), TypeScript checks that event names exist in the map and that arguments match the defined listener parameters, catching errors at compile time rather than runtime.

What is the difference between on() and waitFor() in LINEJS?

The on() method registers a persistent listener that remains active until explicitly removed with off(), while waitFor() creates a temporary one-shot listener that returns a Promise. After the event fires once, waitFor() automatically deregisters its internal listener (as implemented in lines 55-62 of packages/linejs/base/core/typed-event-emitter/index.ts), making it ideal for awaiting specific initialization events like "ready".

Where are the event types defined for the LINEJS Client?

The Client class in packages/linejs/client/client.ts inherits from TypedEventEmitter<ClientEvents>, where ClientEvents is a type mapping event names like "message" and "square:message" to their respective handler signatures. Base client events such as "login" and "update:profile" are defined separately in packages/linejs/base/core/utils/events.ts and exposed through the BaseClient class.

Can I use TypedEventEmitter outside of the LINEJS SDK?

Yes. The TypedEventEmitter class is exported independently in packages/linejs/base/core/typed-event-emitter/index.ts and can be imported for any Deno or TypeScript project requiring strictly-typed event handling. The implementation has no external dependencies on LINE protocol specifics, making it suitable for general-purpose event-driven architectures.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →