Skip to Content
Course content

106: Typing Custom DOM Events

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this pattern a dozen times in code reviews: a developer creates a custom event to pass data between components, and then they try to "force" the type in the event listener signature like this:

// ❌ The "I'll just tell TypeScript it is this" approach
window.addEventListener('user-login', (event: UserLoginEvent) => {
  console.log(event.detail.username); 
});

The Lie of the Event Signature

Here is the problem: addEventListener is defined by the DOM API to expect a callback that takes a generic Event. When you change that signature to UserLoginEvent, you aren't actually telling TypeScript how to handle the event; you're lying to the compiler about what the DOM is actually going to send you.

If another part of your app accidentally triggers a 'user-login' event without the expected payload, or if a browser extension interferes, your code will crash at runtime because you've bypassed the type safety check. You've essentially used a type assertion without the as keyword, which is a dangerous habit to get into.

Leveraging the CustomEvent Generic

The correct way to handle this is to realize that the DOM provides a built-in CustomEvent interface. The trick is that CustomEvent accepts a generic type specifically for the detail property, which is where your custom data lives.

Let's say we're building a notification system where a "Toast" event carries a message and a priority level. Instead of fighting the event listener, define the shape of your data first:

interface ToastDetail {
  message: string;
  priority: 'low' | 'high';
}

// We create a type alias that extends CustomEvent with our detail shape
type ToastEvent = CustomEvent<ToastDetail>;

// Now, in the listener, we cast the event inside the body or use a type guard
window.addEventListener('show-toast', (event) => {
  const toastEvent = event as ToastEvent;
  console.log(`[${toastEvent.detail.priority}] ${toastEvent.detail.message}`);
});

I prefer casting inside the function body (or using a type guard) because it explicitly acknowledges that the event coming from the DOM is untrusted until we verify it.

Creating Type-Safe Dispatchers

Typing the listener is only half the battle. You also want to make sure you can't dispatch a custom event with the wrong data. I usually wrap the dispatchEvent call in a small helper function. This prevents you from having to remember the exact structure of the detail object every time you fire the event.

function dispatchToast(message: string, priority: 'low' | 'high') {
  const event = new CustomEvent<ToastDetail>('show-toast', {
    detail: { message, priority }
  });
  window.dispatchEvent(event);
}

// Now it's impossible to send a 'medium' priority or forget the message
dispatchToast('Connection lost!', 'high'); 

By decoupling the event definition from the DOM's generic Event type, you get full autocomplete in your listeners and a compile-time guarantee that your event producers are sending the right data. It's a bit more boilerplate up front, but it saves you from those "cannot read property 'x' of undefined" errors that haunt large-scale DOM projects.




📋 Practical Task

Implement a Type-Safe Theme Switcher Event

You are building a theme management system. Your goal is to implement a custom event called theme-changed that allows different parts of the UI to react when the user switches themes.

Requirements:

  • Define an interface ThemeDetail that contains themeName (a string union of 'light', 'dark', or 'solarized') and timestamp (a number).
  • Create a type alias ThemeChangeEvent that extends CustomEvent using the ThemeDetail interface.
  • Write a function changeTheme(newTheme: 'light' | 'dark' | 'solarized') that dispatches the theme-changed event with the current timestamp.
  • Implement a window.addEventListener for theme-changed that safely casts the event and logs: "Theme changed to [themeName] at [timestamp]".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.