SyntheticEvent, ChangeEvent and MouseEventHandler without any
An event handler without a correct type almost automatically tempts you into any or repeated type casts the moment you access event.target.value. React event handler types fix this problem at the root: SyntheticEvent, ChangeEvent, and the matching handler types like MouseEventHandler know exactly which element triggered an event and which properties are available on it. This article shows how to apply React event handler types systematically, without any.
Table of Contents
- 1. Why React events need their own types
- 2. Understanding SyntheticEvent: React event vs. native DOM event
- 3. The most common handler types: onClick, onChange, onSubmit
- 4. Generic event handlers for form fields
- 5. Distinguishing currentTarget from target type-safely
- 6. Event handlers as props: MouseEventHandler and friends
- 7. Handling keyboard and focus events type-safely
- 8. Custom callback signatures instead of native events
- 9. Event handler typing compared
- 10. Summary
- 11. FAQ
1. Why React events need their own types
React does not pass browser events directly to event handlers, but wraps them in its own wrapper object, SyntheticEvent. This wrapper object normalizes differences between browsers and ensures consistent behavior across every target platform. For typing, that means native DOM types like Event or MouseEvent from lib.dom.d.ts do not automatically fit React events, and dedicated, React-specific event handler types are needed instead, which React itself provides through the @types/react package.
Without these specific types, many developers reach either for any as the event parameter or for an overly generic Event, both of which cause properties like event.target.value to no longer be type-checked. A typo in the property chain then only surfaces at runtime, often as undefined that gets silently processed further instead of appearing as a compile error directly in the editor. The correct React event handler types prevent exactly that, because they precisely describe which element triggered the event and which properties exist on that element.
Another reason for precise event types is editor autocomplete. Once an event handler is correctly typed, the editor suggests exactly the available properties on event., including specific fields like key for keyboard events or clientX for mouse events. This support disappears completely once the parameter is declared as any.
2. Understanding SyntheticEvent: React event vs. native DOM event
SyntheticEvent is the base class of every React event type and captures the shared set of properties that practically every event has: preventDefault(), stopPropagation(), target, and currentTarget. For concrete event kinds, specialized subtypes exist, like MouseEvent, KeyboardEvent, ChangeEvent, and FormEvent, each imported from the react package, not from the global DOM type definitions. The name collision with the identically named native DOM types is deliberate: React signals that this is the React-specific variant, which is why the import must come explicitly from react.
A common misunderstanding concerns reusing event objects outside the synchronous handler call, for example in a setTimeout or after an await. In older React versions, SyntheticEvent objects were recycled after the handler call, in current versions this pooling has been removed, but accessing event.target after an asynchronous operation still remains risky, because the underlying DOM element may have changed in the meantime. Anyone who needs a value from the event across an asynchronous boundary should therefore extract it synchronously into a local variable.
import type { ChangeEvent, MouseEvent, FormEvent } from "react";
// SyntheticEvent types are imported from "react", not from lib.dom.d.ts
function handleClick(event: MouseEvent<HTMLButtonElement>) {
event.preventDefault();
console.log(event.currentTarget.name); // typed: HTMLButtonElement
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
console.log(event.target.value); // typed: string
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
// Extract needed values synchronously before any async boundary
const formData = new FormData(event.currentTarget);
console.log(formData.get("email"));
}
3. The most common handler types: onClick, onChange, onSubmit
For the three most common interactions in React applications, there are fixed, well-documented event handler types: MouseEvent<HTMLButtonElement> for click handlers on buttons, ChangeEvent<HTMLInputElement> for changes to input fields, and FormEvent<HTMLFormElement> for submitting a form. The generic type parameter in angle brackets in each case states which HTML element the handler is actually attached to, and thereby determines the concrete type of event.currentTarget.
It matters to correctly match the type parameter to the actual element: an onClick handler on a <div> needs MouseEvent<HTMLDivElement>, not HTMLButtonElement, otherwise the compiler fails on access to button-specific properties like disabled, or worse, silently allows an incorrect access if both element types happen to share the same property. This precision pays off especially in more complex interactions where a handler could be attached to several different element types.
import { useState } from "react";
import type { ChangeEvent, FormEvent } from "react";
function LoginForm() {
const [email, setEmail] = useState("");
// Typed to the exact element the handler is attached to
const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value);
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log("Submitting", email);
};
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={handleEmailChange} />
<button type="submit">Sign in</button>
</form>
);
}
4. Generic event handlers for form fields
Forms with many individual fields often lead to repeated, nearly identical onChange handlers, each differing only in the field name inside the setState call. Instead of writing a separate handler for every field, a single, generic handler can be implemented that reads the field name from the input element's name attribute. The event handler type stays unchanged as ChangeEvent<HTMLInputElement>, while the generic state type T describes which fields the given form actually has.
This technique significantly reduces duplication but requires a deliberate decision at one spot: accessing event.target.name as a key into a generic state object is inherently a bit less strictly typed than a handler per field, because TypeScript cannot guarantee at compile time that every name value in the JSX actually corresponds to a field in the state type. This trade-off should be taken deliberately, usually via a single, clearly visible type assertion at the spot where the field name is used as a key, instead of carrying the uncertainty unnoticed throughout the whole component.
import { useState } from "react";
import type { ChangeEvent } from "react";
interface RegistrationForm {
email: string;
username: string;
city: string;
}
function RegistrationFields() {
const [form, setForm] = useState<RegistrationForm>({ email: "", username: "", city: "" });
// One generic handler instead of one handler per field
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setForm((prev) => ({ ...prev, [name as keyof RegistrationForm]: value }));
};
return (
<>
<input name="email" value={form.email} onChange={handleChange} />
<input name="username" value={form.username} onChange={handleChange} />
<input name="city" value={form.city} onChange={handleChange} />
</>
);
}
5. Distinguishing currentTarget from target type-safely
A detail that is regularly overlooked when typing event handlers is the difference between event.target and event.currentTarget. currentTarget is always exactly the element the handler was registered on, and carries precisely the generic type parameter stated on the event type, for example HTMLButtonElement. target, on the other hand, is the element that originally triggered the event, which during event bubbling can be any child element of the registered element, and is therefore typically typed as a broader EventTarget, which offers fewer specific properties.
For most event handler types in React, currentTarget is the correct choice, because it reliably points to the element with the known, expected structure. A click handler on a <button> that accesses a data attribute via currentTarget.dataset gets exactly the properties of an HTMLButtonElement suggested by the compiler. Accessing target instead, because the event was actually triggered by a nested <span> inside the button, requires an explicit type cast or a type guard to safely use button-specific properties.
import type { MouseEvent } from "react";
function IconButton() {
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
// currentTarget: always the button the handler is attached to
console.log(event.currentTarget.dataset.action);
// target: whichever element within the button actually triggered the event
// (could be an inner <svg> or <span>), typed more loosely as EventTarget
if (event.target instanceof HTMLElement) {
console.log(event.target.tagName);
}
};
return (
<button data-action="delete" onClick={handleClick}>
<span>Delete</span>
</button>
);
}
6. Event handlers as props: MouseEventHandler and friends
When an event handler is not defined inside the component itself, but passed in as a prop from outside, the single-word handler type family with the Handler suffix is the more compact alternative to the spelled-out function signature. MouseEventHandler<HTMLButtonElement> is functionally identical to (event: MouseEvent<HTMLButtonElement>) => void, but reads much shorter in a props interface and makes it immediately clear that this is an event handler and not an arbitrary callback.
This compact notation exists for practically every common event type: ChangeEventHandler, FormEventHandler, KeyboardEventHandler, FocusEventHandler. All follow the same pattern and expect the same generic element type as their event counterpart. For props interfaces with several event handlers, this naming convention significantly improves readability, because the Handler suffix makes it immediately recognizable in the interface what kind of prop it is, without having to read the full function signature.
import type { MouseEventHandler, ChangeEventHandler } from "react";
interface EditableRowProps {
onDelete: MouseEventHandler<HTMLButtonElement>;
onLabelChange: ChangeEventHandler<HTMLInputElement>;
label: string;
}
// Equivalent to: onDelete: (event: MouseEvent<HTMLButtonElement>) => void
function EditableRow({ onDelete, onLabelChange, label }: EditableRowProps) {
return (
<div>
<input value={label} onChange={onLabelChange} />
<button onClick={onDelete}>Delete</button>
</div>
);
}
7. Handling keyboard and focus events type-safely
KeyboardEvent adds specific fields on top of SyntheticEvent's base properties, like key, code, and the modifier flags shiftKey, ctrlKey, and altKey. For keyboard interactions, for example confirming a form with the Enter key or closing a dialog with Escape, event.key is checked against a string, and here too the compiler suggests the exact property names instead of relying on outdated, long-deprecated properties like keyCode.
FocusEvent follows the same pattern for onFocus and onBlur handlers and additionally adds the field relatedTarget, which on onBlur describes the element focus is moving to next. A typical use case is a dropdown that should close as soon as focus leaves the dropdown and its children entirely, but stay open as long as relatedTarget is still within the dropdown container.
import { useRef } from "react";
import type { KeyboardEvent, FocusEvent, ReactNode } from "react";
function SearchBox() {
// Keyboard event: "key" is a proper string, no deprecated keyCode needed
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
console.log("Search confirmed");
}
if (event.key === "Escape") {
event.currentTarget.blur();
}
};
return <input type="text" onKeyDown={handleKeyDown} />;
}
function Dropdown({ children }: { children: ReactNode }) {
const containerRef = useRef<HTMLDivElement>(null);
// relatedTarget: the element focus is moving to, may be null
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
if (!containerRef.current?.contains(event.relatedTarget)) {
console.log("Focus left the dropdown entirely, closing it");
}
};
return (
<div ref={containerRef} onBlur={handleBlur} tabIndex={-1}>
{children}
</div>
);
}
8. Custom callback signatures instead of native events
Not every callback a component exposes to the outside should necessarily forward a native SyntheticEvent. A select component that picks an option, or a rating component that produces a star value, communicates more sensibly with the outside world via its own, domain-specific callback signature, for example onSelect: (option: Product) => void, instead of passing through the internal ChangeEvent or MouseEvent unchanged. The calling component cares about the selected product, not implementation details like the underlying native element.
This decoupling has a practical benefit for event handler types: if the internal HTML element a component uses to implement an interaction changes, for example switching from a native <select> to a list of <div> elements for more styling freedom, the externally visible callback signature remains completely unaffected. The component internally extracts the relevant data from the respective native event and forwards only that data through its own, stable signature.
interface Product { id: number; name: string; price: number; }
interface ProductPickerProps {
products: Product[];
// Domain-specific callback instead of a raw native event
onSelect: (product: Product) => void;
}
function ProductPicker({ products, onSelect }: ProductPickerProps) {
// Internal native event is translated into the domain callback here
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const selected = products.find((p) => String(p.id) === event.target.value);
if (selected) onSelect(selected);
};
return (
<select onChange={handleChange}>
{products.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
);
}
// Caller only ever sees a typed Product, never the underlying ChangeEvent
// <ProductPicker products={products} onSelect={(product) => console.log(product.price)} />
9. Event handler typing compared
The following overview summarizes which approach to typing event handlers fits which scenario.
| Scenario | Unsuitable approach | Recommended approach | Benefit |
|---|---|---|---|
| Handler defined directly in the component | (event: any) => void |
(event: ChangeEvent<HTMLInputElement>) => void |
Autocomplete and checked properties |
| Handler passed in as a prop | spelled-out function signature | MouseEventHandler<HTMLButtonElement> |
More compact, immediately recognizable as a handler |
| Accessing the registered element | event.target |
event.currentTarget |
Reliably the type of the registered element |
| Public component API | forwarding the native event unchanged | custom callback signature like onSelect(item: T) |
Internal implementation stays swappable |
| Many nearly identical field handlers | one handler per form field | one generic handler based on name |
Significantly less duplicated code |
No single approach is universally right. For simple, local handlers, the concrete event typing is entirely sufficient. But once a handler becomes visible to the outside as a prop, either the compact handler type family or, for domain-specific components, a custom callback signature that deliberately hides native event details pays off.
Mironsoft
TypeScript tooling, type-safe React forms and Magento/Hyvä integrations
Event handlers without any, but with full type checking?
We review existing React components for loosely typed events, replace any handlers with correct SyntheticEvent types, and build type-safe form and interaction logic for your frontend.
Event audit
Reviewing existing handlers for any and loose event types
Form refactoring
Introducing generic, type-safe handlers for complex forms
Component API
Domain-specific callback signatures instead of raw events
10. Summary
React event handler types start with understanding that React provides its own event types, imported from the react package, which differ from native DOM types. MouseEvent, ChangeEvent, and FormEvent, together with their generic element type parameter, cover the most common interactions, while the compact Handler type family fits event handlers passed out as props. currentTarget remains the more reliable choice over target for most cases, because it carries exactly the type of the registered element.
For forms with many fields, a generic handler based on the name attribute significantly reduces duplication, but requires a deliberate, visible type assertion at a single spot. Public component APIs benefit from replacing native events with their own, domain-specific callback signatures, so internal implementation details stay swappable without changing the externally visible interface. Anyone who applies these principles consistently replaces any handlers with code that reports typos and incorrect property access already at compile time.
React Event Handler Types - The Essentials at a Glance
SyntheticEvent
React's own event types from the react package, not from lib.dom.d.ts, with a generic element type parameter.
currentTarget vs. target
currentTarget reliably carries the type of the registered element, target is more broadly typed during bubbling.
Handlers as props
MouseEventHandler, ChangeEventHandler, and friends as a compact alternative to the spelled-out function signature.
Public APIs
Domain-specific callbacks instead of raw events, so internal implementation details stay swappable.