Custom events carry data through detail, but TypeScript doesn't know its shape automatically. This lesson covers patterns for giving your dispatched events real compile-time type safety.
Typing a CustomEvent's detail
CustomEvent is a generic type in TypeScript's DOM type definitions: CustomEvent<T>, where T is the shape of detail. Explicitly typing the events you dispatch (and, ideally, exporting a matching type alias) gives both your own component's code and any consumer's event listener accurate, checked access to event.detail's fields.
Without this, event.detail inside a listener is typed as any by default (from the base, generic CustomEvent type), silently losing all type safety exactly where a typo or shape mismatch is easy to miss.
The satisfies ItemSelectedEvent check confirms the constructed event actually matches the exported type, catching a mismatched detail shape at compile time.
Consuming a Typed Custom Event
import type { ItemSelectedEvent } from './item-list.js';
element.addEventListener('item-selected', (event: ItemSelectedEvent) => {
console.log(event.detail.id); // fully typed, autocompletes correctly
});
Export both a detail interface and a CustomEvent<Detail> type alias for every custom event your component dispatches.
Consumers import the type alias and annotate their listener's parameter to get full autocomplete and type checking on event.detail.
addEventListener's built-in overloads won't automatically know about your custom event names — the explicit parameter annotation is what provides the typing.
The satisfies operator (TypeScript 4.9+) is a convenient way to verify a constructed event matches its expected type without changing the expression's inferred type.
Typed Events Cheat Sheet
The pattern to repeat for every custom event a component dispatches.
Step
Code
1. Define the detail shape
export interface FooDetail { ... }
2. Export an event type alias
export type FooEvent = CustomEvent<FooDetail>;
3. Dispatch with a type check
new CustomEvent('foo', { detail }) satisfies FooEvent
4. Consumer imports the type
import type { FooEvent } from './my-el.js';
5. Consumer annotates the listener
(event: FooEvent) => { ... }
Advanced: Augmenting HTMLElementEventMap
For an even more seamless experience, you can use TypeScript's declaration merging to add your custom event name directly to the global HTMLElementEventMap interface, so addEventListener('item-selected', ...) is typed correctly automatically, without the consumer needing an explicit type annotation at all.
declare global {
interface HTMLElementEventMap {
'item-selected': ItemSelectedEvent;
}
}
// Now this is automatically typed, no annotation needed:
element.addEventListener('item-selected', (event) => {
console.log(event.detail.id); // TypeScript already knows the shape
});
This technique is genuinely more ergonomic for consumers but adds complexity to your component library's own typings setup — reserve it for published, widely-consumed component libraries.
Typing Events Bound Inside Lit Templates
When listening for a custom event with @event-name=${handler} directly inside another Lit component's own template, TypeScript infers the parameter type from how you write the handler — annotate it explicitly with your exported event type for the same compile-time safety as a manually attached listener.
Leaving event.detail typed as any by never specifying a generic type parameter for CustomEvent, losing type safety at the exact point it matters most.
Defining a detail interface but forgetting to export it, forcing consumers to redefine the same shape themselves.
Constructing a CustomEvent with a detail shape that doesn't actually match its declared type, without any compile-time check catching the mismatch.
Augmenting the global HTMLElementEventMap for internal, application-only components where the added complexity isn't worth the ergonomic benefit.
Key Takeaways
CustomEvent<T> is generic — always specify T (the detail shape) for events you dispatch.
Export both the detail interface and a matching event type alias for consumers to use.
The satisfies operator is a convenient way to verify a dispatched event matches its expected shape.
HTMLElementEventMap augmentation gives the most seamless typing experience, best reserved for published component libraries.
Pro Tip
Treat every custom event's detail type export with the same care as a public function's return type — it's the exact contract consumers will write code against, and a well-typed, well-named detail interface often serves as better documentation than a paragraph of prose ever could.
You've completed the TypeScript series. Next, learn techniques for optimizing Lit component performance.