- Published on
Build Your Own Event Emitter
- Authors
- Name
- Muyiwa Johnson

Introduction
As someone who loves building software, I'm always looking for patterns that make my code cleaner, more modular, and easier to maintain. One of the most powerful tools I've discovered? Event emitters.
At first, I used them without really understanding how they worked—just copying snippets from libraries or frameworks. But when I finally took the time to dig deeper, everything clicked. Event emitters became my go-to solution for managing communication between different parts of an application without creating messy dependencies.
Understanding Event Emitters
An event emitter is essentially a pub-sub (publish-subscribe) system where:
- Components can subscribe to events
- Other components can emit those events
- The emitter handles notifying all subscribers
This creates beautiful decoupling between different parts of your application.
Building Our Own Event Emitter
Let's implement a type-safe event emitter in TypeScript:
// Define a generic type for the event map
type EventMap = Record<string, any>
// The EventEmitter class, generic over the event map
class NewEventEmitter<TEvents extends EventMap> {
// Use a Map to store listeners: eventName -> Set of callbacks
private listeners = new Map<keyof TEvents, Set<(data: any) => void>>()
// Subscribe to an event
subscribe<K extends keyof TEvents>(eventName: K, callback: (data: TEvents[K]) => void) {
// Ensure a Set exists for this event
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, new Set())
}
// Add the callback to the Set
this.listeners.get(eventName)?.add(callback)
// Return an unsubscribe function for easy cleanup
return () => this.unsubscribe(eventName, callback)
}
// Unsubscribe from an event
unsubscribe<K extends keyof TEvents>(eventName: K, callback: (data: TEvents[K]) => void) {
// Remove the callback from the Set for this event
this.listeners.get(eventName)?.delete(callback)
}
// Emit an event, calling all subscribed listeners
emit<K extends keyof TEvents>(eventName: K, data: TEvents[K]) {
// Iterate over the Set of callbacks and execute each one
this.listeners.get(eventName)?.forEach((callback) => {
callback(data)
})
}
// Note: A 'once' method could be added here as well,
// similar to the previous implementation, by wrapping the callback
// and calling unsubscribe within the wrapper.
}
This implementation uses a Map
where keys are event names and values are Set
s of callback functions. Using a Set
automatically handles duplicate subscriptions and provides efficient addition/deletion. The subscribe
method now conveniently returns an unsubscribe
function.
How i have used Event Emitters
1. UI Component Communication
// Define our events
type ButtonEvents = {
click: { x: number; y: number }
hover: boolean
}
class Button {
// Use the NewEventEmitter
private emitter = new NewEventEmitter<ButtonEvents>()
constructor(private element: HTMLElement) {
element.addEventListener('click', (e) => {
this.emitter.emit('click', {
x: e.clientX,
y: e.clientY,
})
})
element.addEventListener('mouseenter', () => {
this.emitter.emit('hover', true)
})
element.addEventListener('mouseleave', () => {
this.emitter.emit('hover', false)
})
}
// Use 'subscribe' instead of 'on'
onClick(callback: (pos: { x: number; y: number }) => void) {
return this.emitter.subscribe('click', callback)
}
onHover(callback: (isHovering: boolean) => void) {
return this.emitter.subscribe('hover', callback)
}
}
// Usage
const buttonElement = document.getElementById('my-btn')
if (buttonElement) {
const button = new Button(buttonElement)
const unsubscribeClick = button.onClick(({ x, y }) => {
console.log(`Clicked at (${x}, ${y})`)
})
// Call the returned function to unsubscribe
unsubscribeClick() // Clean it up
}
2. State Management (My Favorite Use Case)
type StoreEvents<T> = {
change: T
error: string // Example error event
}
class SimpleStore<T> {
// Use NewEventEmitter
private emitter = new NewEventEmitter<StoreEvents<T>>()
private state: T
constructor(initialState: T) {
this.state = initialState
}
setState(newState: T) {
this.state = newState
this.emitter.emit('change', newState)
}
// The primary method is 'subscribe'
subscribe(callback: (state: T) => void) {
// Immediately call with current state
callback(this.state)
// Subscribe to future changes
return this.emitter.subscribe('change', callback)
}
// Example method to emit an error
setError(errorMessage: string) {
this.emitter.emit('error', errorMessage)
}
// Method to subscribe specifically to errors
onError(callback: (error: string) => void) {
return this.emitter.subscribe('error', callback)
}
}
// Usage
const store = new SimpleStore({ count: 0 })
const unsubscribeState = store.subscribe((state) => {
console.log('State changed:', state)
})
const unsubscribeError = store.onError((error) => {
console.error('Store error:', error)
})
store.setState({ count: 1 }) // Logs: "State changed: { count: 1 }"
store.setError('Something went wrong!') // Logs: "Store error: Something went wrong!"
// Later, cleanup subscriptions if needed
// unsubscribeState();
// unsubscribeError();
Why I Love This Pattern
- Decoupling: Components don't need to know about each other
- Flexibility: Easy to add new listeners without modifying existing code
- Type Safety: TypeScript prevents common mistakes
- Performance: Lightweight compared to many alternatives
- Debugging: Easy to track event flow through the system
Common Pitfalls to Avoid
Pitfall | Solution |
---|---|
Memory Leaks | Always call the unsubscribe function returned by subscribe |
Too Many Events | Keep your event types focused and meaningful |
Complex Payloads | Keep event data simple and structured |
Nice Practices i picked up along the way
Clean up subscriptions using the returned function:
// React example useEffect(() => { const unsubscribe = emitter.subscribe('event', handler) // Cleanup function runs on unmount return () => unsubscribe() }, [])
Use TypeScript with
EventMap
for self-documenting events.Keep events focused on specific actions or state changes.
Document your event system for team members.
Conclusion
Event emitters have become one of my most trusted tools for building maintainable software. They're particularly useful when:
- You need communication between decoupled components
- You want to avoid complex dependency chains
- You need a flexible system that can evolve over time
The TypeScript implementation we built provides excellent type safety while remaining lightweight and flexible. I encourage you to try building your own event emitter and see where it might simplify your codebase!