Type Event Handlers in TypeScript
Learn how to type event handler parameters in TypeScript so the compiler knows which properties exist on click, input, keyboard, and other DOM events.
When you call element.addEventListener with a standard event name like "click" or "keydown", TypeScript already knows what type the event parameter should be. You do not need to write a type annotation on the callback. The compiler maps every standard DOM event name to its event interface, and the callback parameter gets that type automatically.
How event type inference works
TypeScript uses a mapping called HTMLElementEventMap that connects event name strings to event interfaces. When you pass a standard event name to addEventListener, the callback parameter type is inferred from that mapping.
const button = document.querySelector('button');
if (button === null) throw new Error('Button not found');
button.addEventListener('click', (event) => {
// ^? (parameter) event: MouseEvent
console.log(event.clientX, event.clientY);
});The event parameter is MouseEvent without you writing the type. TypeScript inferred it from the string "click". Here is how common event names map to their types:
| Event name | Inferred event type |
|---|---|
| click, dblclick, mousedown | MouseEvent |
| keydown, keyup | KeyboardEvent |
| input, change | Event |
| submit | SubmitEvent |
| focus, blur | FocusEvent |
| scroll | Event |
Keyboard events
When you listen for keydown or keyup, the event is KeyboardEvent. You get key, code, and modifier properties without any extra typing work.
document.addEventListener('keydown', (event) => {
// ^? (parameter) event: KeyboardEvent
console.log(event.key); // 'Enter', 'a', 'Escape'
console.log(event.code); // 'KeyA', 'Escape'
console.log(event.ctrlKey); // boolean
});KeyboardEvent adds the logical key value through .key and the physical key code through .code. Both are available because TypeScript knows the event type from the event name alone. You never need to cast or annotate the parameter.
Input and change events
Input and change events are typed as Event, not a more specific type like InputEvent. This follows the DOM specification where input events use the base Event interface. The base Event type does not have .target.value, so you need to narrow the target.
TypeScript does not narrow event.currentTarget to the element you attached the listener to. Both event.target and event.currentTarget keep the generic EventTarget or null type, no matter which element type the listener is bound to:
const nameInput = document.querySelector<HTMLInputElement>('#name');
if (nameInput === null) throw new Error('Input not found');
nameInput.addEventListener('input', (event) => {
const input = event.currentTarget;
// ^? const input: EventTarget | null
});The simplest fix is to skip currentTarget and read the outer variable instead, since the callback still has access to nameInput through the closure:
nameInput.addEventListener('input', () => {
console.log(nameInput.value); // OK, nameInput is already HTMLInputElement
});If you need the value from the event object itself, assert the type because you know which element the listener is attached to:
nameInput.addEventListener('input', (event) => {
const input = event.currentTarget as HTMLInputElement;
console.log(input.value); // OK
});event.target vs event.currentTarget
These two properties serve different purposes, but TypeScript types both of them as EventTarget or null. Narrow or assert either one before reading element-specific properties.
| Property | Type | What it points to |
|---|---|---|
| event.target | EventTarget or null | The element that fired the event (may be a child) |
| event.currentTarget | EventTarget or null | The element the listener is attached to |
When a click on a span inside a button bubbles up, event.target is the span but event.currentTarget is the button. Prefer currentTarget when you need to interact with the element that has the listener. Narrow target with instanceof when you need the exact source element:
document.addEventListener('click', (event) => {
if (event.target instanceof HTMLButtonElement) {
console.log(event.target.disabled); // OK
}
});Submit events
Submit events give you SubmitEvent, which includes a submitter property that identifies which button triggered the submission. This is useful when a form has multiple submit buttons with different purposes like "Save Draft" and "Publish."
const form = document.querySelector<HTMLFormElement>('#signup');
if (form === null) throw new Error('Form not found');
form.addEventListener('submit', (event) => {
event.preventDefault();
const submitter = event.submitter;
console.log(submitter);
});SubmitEvent extends Event and adds submitter, typed as HTMLElement or null. Narrow it with instanceof to access button-specific properties like name, which tells you which button the user clicked:
if (submitter instanceof HTMLButtonElement) {
console.log(submitter.name); // e.g., 'save-draft' vs 'publish'
}Custom events
For custom events, use CustomEvent with a generic describing the detail payload. The generic only applies when you create the event with new CustomEvent, so dispatching is fully typed:
interface CartUpdate {
productId: string;
quantity: number;
}
const cartEvent = new CustomEvent<CartUpdate>('cart-update', {
detail: { productId: 'abc-123', quantity: 2 },
});
document.dispatchEvent(cartEvent);Listening is a different story. addEventListener does not know about custom event names, so the callback parameter is the generic Event type, not CustomEvent. Assert the type inside the listener to get the detail property:
document.addEventListener('cart-update', (event) => {
const cartUpdateEvent = event as CustomEvent<CartUpdate>;
console.log(cartUpdateEvent.detail.productId); // 'abc-123'
console.log(cartUpdateEvent.detail.quantity); // 2
});This assertion is safe as long as you only dispatch 'cart-update' with a CartUpdate detail elsewhere in your code. TypeScript cannot verify that connection automatically, since string event names are not tracked between dispatchEvent and addEventListener.
Common mistakes
Reading .value directly from event.target or event.currentTarget without narrowing is the most common error. Both stay typed as EventTarget or null, not an input element, so .value does not exist on either without a type assertion or an instanceof check.
Using the wrong event name for the expected type is also frequent. The string "keydown" gives KeyboardEvent, not InputEvent. Check the event mapping table above if the type does not match what you expect.
Forgetting that a type assertion does not check anything at runtime leads to avoidable crashes. Asserting event.currentTarget as HTMLInputElement only silences the compiler. Use instanceof instead when the listener could be attached through delegation to more than one element type.
See Type DOM Elements in TypeScript for element type basics. See Type Form Elements in TypeScript for form-specific event patterns.
Rune AI
Key Insights
- TypeScript infers the event type from the event name: 'click' gives MouseEvent, 'keydown' gives KeyboardEvent.
- The callback parameter is automatically typed when you use a standard event name.
- event.target and event.currentTarget are both typed as EventTarget or null, even when you know which element the listener is attached to.
- Narrow event.target or event.currentTarget with instanceof, or assert the element type, before reading element properties.
- Custom events use CustomEvent with a generic for dispatching, then a type assertion inside the listener to read the detail property.
Frequently Asked Questions
How does TypeScript know which event type to use for addEventListener?
Can I type custom events in TypeScript?
Why does event.target have type EventTarget instead of the element type?
Conclusion
TypeScript maps every standard DOM event name to its event type automatically. When you write element.addEventListener with a standard event name, the callback parameter is already typed. For custom events, use CustomEvent with a generic and cast inside the listener. event.target and event.currentTarget both need narrowing or a type assertion before you read element properties.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.