Getting React Event Handler Types Right
AI generated
<T>
type
TypeScript · React · Events · Forms
Getting React Event Handler Types Right
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.

13 min read SyntheticEvent · ChangeEvent · MouseEventHandler TypeScript 5.x · React 18/19

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.

11. FAQ: React Event Handler Types

1Why not use native DOM event types?
React wraps events in SyntheticEvent for normalization. The matching types must be imported from the react package.
2What does the parameter in ChangeEvent<HTMLInputElement> mean?
It determines which element the handler is attached to and the type of event.currentTarget.
3target vs. currentTarget?
currentTarget is always the registered element with the stated type. target is the originally triggering element, more broadly typed.
4When MouseEventHandler instead of a function signature?
When the handler is passed as a prop. Shorter and immediately recognizable as an event handler.
5Generic handler for many form fields?
A ChangeEvent handler using name as the key into the state object, with a deliberate assertion at one spot.
6Why not just forward native events?
Internal implementation changes would otherwise affect the public interface. A custom callback signature stays stable.
7Is event.target safe after await?
Risky, the DOM element may have changed. Extract needed values synchronously beforehand.
8Extra properties on KeyboardEvent?
key, code, and the modifier flags shiftKey, ctrlKey, altKey, instead of deprecated properties like keyCode.
9What does relatedTarget provide on FocusEvent?
On onBlur, the element focus is moving to. Useful for checking whether a container is really being left.
10Why is any problematic as an event type?
Switches off every type check. Typos in the property chain surface only at runtime, not already at compile time.