Typed Custom Events: Making Browser Events Type-Safe

CustomEvent lets you fire your own browser events, but its payload is `any`. Typing them means defining a specific shape for the event's `detail` property, turning a generic signal into a predictable, self-documenting API for your components.
WHY IT EXISTS Browser events like 'click' are powerful for decoupling. For application-specific logic, we need our own events. The browser's CustomEvent API provides this, but its detail property, which holds the data payload, is untyped (any) by default. This creates a hole in a TypeScript application's type safety, forcing developers to guess the shape of event data.
THE MENTAL MODEL A typed CustomEvent is a contract. You're telling the rest of your application, "An event named 'order-submitted' will be fired, and when it is, its detail property will ALWAYS contain an object with an orderId (string) and amount (number)." This turns a generic mechanism into a predictable, self-documenting API that your tools can understand.
HOW IT WORKS You use TypeScript generics. The standard CustomEvent interface can be parameterized with the type of its detail property: CustomEvent<T>. When creating an event, you instantiate it as new CustomEvent<MyPayload>('event-name', { detail: myPayload }). When listening for the event, you must cast the received event object to your specific type to access the payload safely: (event as CustomEvent<MyPayload>).detail. This gives you full type-checking and autocomplete on the payload within your listener's scope.
WHEN TO USE IT Use typed custom events for communication between loosely coupled parts of your application. This is common in micro-frontend architectures, when integrating vanilla JS components into a typed framework, or for creating a public API for a reusable component. For example, a date picker component could fire a date-selected event with a Date object in its detail, and the consuming application knows exactly what to expect.
WHEN NOT TO USE IT Avoid using events for communication between tightly coupled components that can share state via props, context, or a state management library like Redux or Zustand. Event-based communication is powerful but can be harder to trace than direct state updates. If two components have a clear parent-child relationship, passing props is simpler and more explicit.
ONE CANONICAL EXAMPLE To signal a user login, first define the payload type: type UserLoginPayload = { userId: string; timestamp: number; }. Then, dispatch the event: const loginEvent = new CustomEvent<UserLoginPayload>('user:login', { detail: { userId: 'abc-123', timestamp: Date.now() } }); document.dispatchEvent(loginEvent);. A listener would then cast the event to read the detail: document.addEventListener('user:login', (e) => { const event = e as CustomEvent<UserLoginPayload>; console.log('User logged in:', event.detail.userId); });. The type assertion is the key to safety.
Read the original → developer.mozilla.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.