TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
106: Typing Custom DOM Events
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
ThemeDetailthat containsthemeName(a string union of 'light', 'dark', or 'solarized') andtimestamp(a number). - Create a type alias
ThemeChangeEventthat extendsCustomEventusing theThemeDetailinterface. - Write a function
changeTheme(newTheme: 'light' | 'dark' | 'solarized')that dispatches thetheme-changedevent with the current timestamp. - Implement a
window.addEventListenerfortheme-changedthat safely casts the event and logs: "Theme changed to [themeName] at [timestamp]".
There are no comments for now.