TYPESCRIPT

Type Safe Event Emitter

Generic event emitter with type-safe event handling

TypeScriptEvent DrivenPatterns

Code

type EventMap = Record<string, any>;
type EventKey<T extends EventMap> = string & keyof T;
type EventCallback<T> = (data: T) => void;

class TypedEventEmitter<T extends EventMap> {
  private listeners: {
    [K in keyof T]?: EventCallback<T[K]>[];
  } = {};

  // Subscribe to an event
  on<K extends EventKey<T>>(
    eventName: K,
    callback: EventCallback<T[K]>
  ): () => void {
    __TOKEN_40__ (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName]!.push(callback);

    // Return unsubscribe function
    __TOKEN_44__ () => this.off(eventName, callback);
  }

  // Subscribe once
  once<K extends EventKey<T>>(
    eventName: K,
    callback: EventCallback<T[K]>
  ): () => void {
    const onceCallback: EventCallback<T[K]> = (data) => {
      callback(data);
      this.off(eventName, onceCallback);
    };
    return this.on(eventName, onceCallback);
  }

  // Unsubscribe
  off<K extends EventKey<T>>(
    eventName: K,
    callback: EventCallback<T[K]>
  ): void {
    const callbacks = this.listeners[eventName];
    __TOKEN_54__ (!callbacks) return;
    
    const index = callbacks.indexOf(callback);
    __TOKEN_57__ (index > -1) {
      callbacks.splice(index, 1);
    }
  }

  // Emit event
  emit<K extends EventKey<T>>(eventName: K, data: T[K]): void {
    const callbacks = this.listeners[eventName];
    __TOKEN_61__ (!callbacks) return;
    
    // Create a copy to avoid issues if callbacks are removed during iteration
    callbacks.slice().forEach(callback => {
      try {
        callback(data);
      } __TOKEN_64__ (error) {
        console.error(`Error in event listener for ${eventName}:`, error);
      }
    });
  }

  // Remove all listeners for an event
  removeAllListeners<K extends EventKey<T>>(eventName?: K): void {
    __TOKEN_66__ (eventName) {
      delete this.listeners[eventName];
    } else {
      this.listeners = {};
    }
  }

  // Get listener count
  listenerCount<K extends EventKey<T>>(eventName: K): number {
    return this.listeners[eventName]?.length || 0;
  }
}

// Usage examples
interface AppEvents {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string; reason: string };
  'data:update': { data: any; version: number };
  'error': Error;
  'notification': { title: string; message: string; type: 'info' | 'warning' | 'error' };
}

const emitter = new TypedEventEmitter<AppEvents>();

// Type-safe subscription
const unsubscribe = emitter.on('user:login', (data) => {
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

// One-time listener
emitter.once('notification', (notification) => {
  console.log(`Notification: ${notification.title}`);
});

// Emit with type safety
emitter.emit('user:login', { 
  userId: '123', 
  timestamp: new Date() 
});

emitter.emit('error', new Error('Something went wrong'));

// Clean up
unsubscribe();

// Advanced usage with async handlers
class AsyncEventEmitter<T extends EventMap> extends TypedEventEmitter<T> {
  async emitAsync<K extends EventKey<T>>(eventName: K, data: T[K]): Promise<void> {
    const callbacks = this.listeners[eventName];
    __TOKEN_87__ (!callbacks) return;
    
    const promises = callbacks.slice().map(callback => 
      Promise.resolve().then(() => callback(data))
    );
    
    await Promise.all(promises);
  }
}