Type-Safe CustomEvents: Implementing Typed Native Event Messaging in TypeScript
These articles are AI-generated summaries. Please check the original sources for full details.
TypedEventTarget
The native EventTarget is a built-in browser API providing a fast, framework-agnostic messaging system. Standard CustomEvent interfaces default to an untyped detail property, frequently requiring manual type casting in TypeScript environments.
Why This Matters
While modern frameworks provide proprietary state and event management, they often introduce vendor lock-in and unnecessary runtime overhead. Native APIs like EventTarget offer superior performance and portability, but their lack of inherent type safety for event payloads can lead to fragile codebases; implementing a typed wrapper bridges the gap between native performance and developer productivity.
Key Insights
- Standard CustomEvent interfaces require manual casting (e.g., ‘e as CustomEvent
’) to access payload data safely. - TypedEventTarget utilizes a generic map (M extends Record<string, unknown>) to associate event names with specific data structures.
- The implementation overrides addEventListener and removeEventListener to enforce TEL<M[K]> types while maintaining native compatibility.
- A custom dispatchEvent helper automates the creation of CustomEvent objects, ensuring data payloads match the defined schema.
- This pattern facilitates the ‘generalized singleton’ approach, allowing a single logic class to interact with multiple frameworks via native JS/TS.
Working Examples
Base class for creating a type-safe EventTarget wrapper.
type EventListener<E extends Event> = (evt: E) => void;
interface EventListenerObject<E extends Event> {
handleEvent(evt: CustomEvent<E>): void;
}
type TEL<E> = EventListener<CustomEvent<E>> | EventListenerObject<CustomEvent<E>>;
export class TypedEventTarget<M extends Record<string, unknown>> {
private readonly target = new EventTarget();
addEventListener<K extends keyof M>(
type: K & string,
listener: TEL<M[K]>,
options?: boolean | AddEventListenerOptions,
) {
this.target.addEventListener(type, listener as EventListenerOrEventListenerObject, options);
}
removeEventListener<K extends keyof M>(
type: K & string,
listener: TEL<M[K]>,
options?: boolean | EventListenerOptions,
) {
this.target.removeEventListener(type, listener as EventListenerOrEventListenerObject, options);
}
dispatchEvent<K extends keyof M>(type: K, ...args: M[K] extends void ? [detail?: undefined] : [detail: M[K]]) {
const [detail] = args;
return this.target.dispatchEvent(new CustomEvent(String(type), { detail }));
}
}
Real-world implementation of a shopping cart using the TypedEventTarget.
export interface CartItem {
id: string;
name: string;
price: number;
}
type ShoppingCartEvents = {
'item-added': CartItem;
'item-removed': { id: string };
'cart-cleared': void;
};
export default class ShoppingCart extends TypedEventTarget<ShoppingCartEvents> {
private _items: CartItem[] = [];
addItem(item: CartItem) {
this._items.push(item);
this.dispatchEvent('item-added', item);
}
removeItem(id: CartItem['id']) {
this._items = this._items.filter((item) => item.id !== id);
this.dispatchEvent('item-removed', { id });
}
}
Practical Applications
- Use case: Synchronizing shopping cart state across disparate UI components without framework-specific event emitters.
- Pitfall: Using ‘any’ or manual casting for event ‘detail’ properties, which bypasses TypeScript safety and causes runtime errors during refactoring.
References:
Continue reading
Next article
9 AI Agents Building Products: Inside the reflectt-node Coordination System
Related Content
Solving WebSocket Authentication: Why Cookies Beat Bearer Tokens
Learn why the native browser WebSocket API's lack of custom header support makes HTTP-only cookies the superior choice for secure authentication.
Streamlining Design Systems with salt-theme-gen: An OKLCH-Based Theme Engine
Hasan Sarwer releases salt-theme-gen, a zero-dependency tool generating 21 semantic colors and 32 interactive states from a single primary color.
Mastering JavaScript Asynchrony: From Callbacks to Promises
Learn how JavaScript's non-blocking architecture uses callbacks and promises to handle heavy operations without freezing the UI or server.