Topic 12.4
Typing Events and Handlers
In one line
Inline handlers get their event type for free; separate handler functions use React's event types such as ChangeEvent<HTMLInputElement>, MouseEvent<HTMLButtonElement> and FormEvent<HTMLFormElement>, and event.currentTarget is the typed element the handler is attached to.
Think of it like this
A doorbell and a parcel. When the bell rings, you know which door it is (your door: the one with the bell on it), but not who pressed it or what they're holding. React events are similar: currentTarget is the element your handler is on, so TypeScript knows exactly what it is; target is whatever was really clicked, which could be anything inside, so TypeScript only knows it is "some event target".
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Event handler
- A function React calls when something happens, like
onClickoronChange. - Synthetic event
- React's wrapper around the browser's event. It has the same main properties, plus
nativeEvent. - Contextual typing
- TypeScript giving an inline function its parameter types from where it is written.
- ChangeEvent
- React's type for
onChangeevents.ChangeEvent<HTMLInputElement>hastarget.valueas a string. - MouseEvent
- React's type for click and mouse events, generic on the element:
MouseEvent<HTMLButtonElement>. - FormEvent / SubmitEvent
- Types for
onSubmit.FormEventis the classic one; newer types preferSubmitEvent. - currentTarget
- The element the handler is attached to. Its type is known exactly.
- target
- The element where the event started. It may be a child, so its type is vague.
- MouseEventHandler
- A shortcut type for a function that takes a
MouseEventand returns nothing.
Step by step
01Start inline, hover to learn the type
Inline arrow functions need no annotation. Hover over e in VS Code: it shows ChangeEvent<HTMLInputElement, HTMLInputElement>. Copy that when you move the handler out.
<input value={name} onChange={(e) => setName(e.target.value)} /> // e is typed for you02Annotate separate handlers
Moved out of JSX, the function loses its context. Import the event type from react and give the element type.
import type { ChangeEvent, KeyboardEvent, MouseEvent, FormEvent } from "react";
function handleChange(e: ChangeEvent<HTMLInputElement>) { setName(e.target.value); }
function handleKey(e: KeyboardEvent<HTMLInputElement>) { if (e.key === "Enter") save(); }
function handleClick(e: MouseEvent<HTMLButtonElement>) { console.log(e.clientX); }
function handleSubmit(e: FormEvent<HTMLFormElement>) { e.preventDefault(); }03Recognise the four classic mistakes
These are the real messages. The last one comes from forgetting to import MouseEvent from react, so the global DOM MouseEvent is used instead.
import type { MouseEvent as ReactMouseEvent } from "react";
function handleChange(e) {
// error TS7006: Parameter 'e' implicitly has an 'any' type.
console.log(e.target.value);
}
function handleClick(e: ReactMouseEvent<HTMLButtonElement>) {
console.log(e.target.value);
// error TS2339: Property 'value' does not exist on type 'EventTarget'.
}
function handleDomClick(e: MouseEvent) { // the DOM's MouseEvent, not React's
console.log(e.clientX);
}
<input onChange={handleClick} />
// error TS2322: Type '(e: MouseEvent<HTMLButtonElement, MouseEvent>) => void' is not assignable to type 'ChangeEventHandler<HTMLInputElement, HTMLInputElement>'.
// Types of parameters 'e' and 'event' are incompatible.
// Type 'ChangeEvent<HTMLInputElement, HTMLInputElement>' is missing the following properties from type 'MouseEvent<HTMLButtonElement, MouseEvent>': altKey, button, buttons, clientX, and 14 more.
<button onClick={handleDomClick}>Go</button>
// error TS2322: Type '(e: MouseEvent) => void' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
// Types of parameters 'e' and 'event' are incompatible.
// Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is missing the following properties from type 'MouseEvent': layerX, layerY, offsetX, offsetY, and 16 more.04Use currentTarget
currentTarget is the element the handler is on, with its exact type. target may be a child. Read values from currentTarget.
05Type handler props
For your own components, a callback that passes data is usually best. For a low-level component that wraps an element, pass the event through with a ...Handler type.
import type { ComponentProps, MouseEventHandler } from "react";
type SongRowProps = {
onSelect: (id: number) => void; // domain callback
onRemove?: MouseEventHandler<HTMLButtonElement>; // DOM-style, optional
onPlay?: ComponentProps<"button">["onClick"]; // borrowed from <button>
};06One handler for many fields
Give each field a name and read e.currentTarget.name and .value. A union of element types lets the same handler serve inputs, selects and textareas.
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
const { name, value } = e.currentTarget;
setForm((f) => ({ ...f, [name]: value }));
}Try it yourself
- 1
Clear with Enter
In the form example, change the key handler so Enter in the name field logs
enter pressedand callse.preventDefault(). Predict: does pressing Enter still submit the form? (No:preventDefaulton the keydown stops the implicit submit.) Try it by typing in the field. - 2
Read target instead
In the song example, replace
e.currentTarget.tagNamewithe.target.tagName(remove the cast). The preview still runs. What wouldtscsay? (Property 'tagName' does not exist on type 'EventTarget'.) Why is that a fair complaint? - 3
Give every row a Remove button
Pass
onRemovefor every song. Then removee.stopPropagation()and predict the console when Remove is clicked: two lines,remove clicked...and thenselected song 2, because the click bubbles to the<li>.
Code & diagrams
The script fills the fields and submits. Try it yourself too: type a name, press Escape to clear it, then submit. One `handleChange` serves all three fields because its event type is a union of the three element types. Caution: the computed key `[name]` is a plain `string`, so TypeScript can't check that `plan` only receives `"free"` or `"pro"`; computed keys are a hole in the types.
Preview
Console
Nothing logged yet.Expected console output
submit from <form>: { name: 'Asha', plan: 'pro', bio: 'I like maps' }Clicking the title `<span>` still runs the `<li>` handler (the event bubbles up), so `target` is `SPAN` while `currentTarget` is `LI`. That's why `target` is typed only as `EventTarget` and needs a cast to read `tagName`. The Remove button stops the event, so the row isn't selected too. Inside the inline `onRemove` arrow, `e` is typed for you from `MouseEventHandler<HTMLButtonElement>`.
Preview
Console
Nothing logged yet.Expected console output
currentTarget: LI | target: LI
selected song 1
currentTarget: LI | target: SPAN
selected song 1
remove clicked, button text: Removeimport type { SubmitEvent, ChangeEventHandler } from "react";
// FormEvent still works but is marked deprecated in these types; SubmitEvent is more precise.
function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
e.preventDefault();
console.log(e.submitter?.textContent, e.currentTarget.elements.length);
}
// A handler alias types the whole function, so (e) needs no annotation.
const onName: ChangeEventHandler<HTMLInputElement> = (e) => console.log(e.target.value);
<form onSubmit={handleSubmit}>
<input onChange={onName} />
</form>
// tsc: no errorsBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to import MouseEvent from react
Write function handleClick(e: MouseEvent) { ... } with no import and use it as <button onClick={handleClick}>.
Break #2
Read value from target on a click
In a button's click handler, write setChoice(e.target.value).
Myth vs fact
Myth
You must annotate every event parameter.
Fact
Inline handlers in JSX are contextually typed. Annotate only functions declared separately, or type the variable with a handler alias like ChangeEventHandler<HTMLInputElement>.
Myth
e.target and e.currentTarget are the same thing.
Fact
They are the same only when the event started on the element itself. target is where it started (maybe a child); currentTarget is where the handler is. Their types differ for that reason.
Myth
React events are the browser's events.
Fact
They are synthetic wrappers with the same main API. Their TypeScript types are different from the DOM's types with the same names; the browser's event is on e.nativeEvent.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
In @types/react 19.x,
ChangeEvent<CurrentTarget, Target>has two type parameters andonChangeon<input>isChangeEventHandler<T, HTMLInputElement>. That's why hovers showChangeEvent<HTMLInputElement, HTMLInputElement>. WritingChangeEvent<HTMLInputElement>still works because the second parameter has a default. - ▸
React's
EventHandler<E>type is declared with a "bivariance hack" ({ bivarianceHack(event: E): void }["bivarianceHack"]), so handler parameters are compared in both directions, looser thanstrictFunctionTypes: a handler forMouseEvent<HTMLElement>fits a button, and so would one for a more specific element. Unrelated elements still fail: aMouseEvent<HTMLButtonElement>handler on a<div>gives... is not assignable to type 'MouseEventHandler<HTMLDivElement>'.In your own prop types, property-styleonPick: (id: number) => voidis checked strictly; method-styleonPick(id: number): voidis bivariant. - ▸
A prop typed
() => voidaccepts() => number; the result is ignored. That's whyonClick={() => setCount(c + 1)}is fine even though some setters return values. But it also means TypeScript won't warn if you pass an async function whose returned promise rejects, so handle errors inside async handlers. - ▸
React attaches one listener per event type at the root and dispatches synthetic events through the React tree (Topic 4.1), which is also why events bubble through portals to their React parent.
e.stopPropagation()stops React propagation; the native event may already have reached listeners attached directly to the DOM.
Remember this
- 1
Inline handlers are typed for you. In
<input onChange={(e) => setName(e.target.value)} />, TypeScript knowsonChangeon an<input>takes a change handler, so it givesethe right type automatically. This is called contextual typing: the place a function is written decides its parameter types. Hover overein your editor to see the type: that's the easiest way to learn the names. - 2
Separate handler functions need an annotation, because a function declared on its own has no context:
function handleChange(e)isParameter 'e' implicitly has an 'any' type.in strict mode. React's event types live in thereactpackage and take the element type as a type argument:ChangeEvent<HTMLInputElement>foronChangeon inputs (alsoHTMLSelectElement,HTMLTextAreaElement),MouseEvent<HTMLButtonElement>foronClick,KeyboardEvent<HTMLInputElement>foronKeyDown(e.keyis"Enter","Escape"...),FocusEvent<HTMLInputElement>foronBlur, andFormEvent<HTMLFormElement>foronSubmit. They are React's synthetic events: React's wrapper around the browser's event with the same main properties (preventDefault(),stopPropagation(),key,clientX...), ande.nativeEventholds the browser's own event. - 3
**Import them from
react.** The browser also has global types calledMouseEvent,KeyboardEventandFocusEvent(the DOM versions). If you forget the import,e: MouseEventsilently means the DOM type, and passing the handler toonClickfails with a long message that ends inis missing the following properties from type 'MouseEvent': layerX, layerY, offsetX, offsetY, and 16 more.Writeimport type { MouseEvent } from "react"(or useReact.MouseEvent). - 4
**
currentTargetversustarget.**e.currentTargetis the element the handler is attached to, typedEventTarget & HTMLButtonElement(so.value,.disabledand friends are available).e.targetis the element where the event started, which can be a child (a<span>inside a<li>), so for mouse events it is typed only asEventTargetande.target.valueis an error:Property 'value' does not exist on type 'EventTarget'.PrefercurrentTarget.ChangeEventis a special case: itstargetis typed as the element too, because change events come from the form field itself. - 5
Handler types for props. When a component accepts a handler, choose between two styles. A domain callback says what happened in your app's words and passes plain data:
onSelect: (id: number) => void,onRename?: (name: string) => void. A DOM-style handler passes the event through:onClick?: MouseEventHandler<HTMLButtonElement>(a shortcut for(e: MouseEvent<HTMLButtonElement>) => void), or borrow it from the element:ComponentProps<"button">["onClick"]. Domain callbacks are easier to test and reuse; DOM-style ones fit low-level components likeButton. Optional callbacks are called withonRename?.(name). A=> voidreturn type also accepts functions that return something; the value is ignored. - 6
Version note. In the
@types/react19.x types installed here,FormEventandFormEventHandlerare marked deprecated, because the browser has no "form event": the types now offerSubmitEvent<HTMLFormElement>foronSubmit(withe.submitter, the button that submitted),InputEventforonInput, andChangeEventforonChange.FormEvent<HTMLFormElement>still compiles everywhere (you'll see it in most code and docs), and your editor shows it struck through. With React 19 form actions (<form action={fn}>, Phase 11) you often don't need a submit event at all.
Explain it without notes
Why don't inline handlers need event types, but separate handler functions do?
What's the difference between e.target and e.currentTarget, and why are they typed differently?
How would you type an onSelect prop and an onClick prop on your own components?
Why can e: MouseEvent fail to match onClick even though the names match?
Practice
Write a SearchInput with a separate handleChange typed with ChangeEvent<HTMLInputElement> and a handleKeyDown that logs search: <text> when Enter is pressed.
Write a Tabs component with props tabs: string[] and onChange: (index: number) => void, which renders a button per tab and calls onChange with the index.
Trade-offs
- ↔
Domain callbacks (
onSelect(id)) hide DOM details and are easy to test, but callers can'tpreventDefaultor read the event. DOM-style handlers give full control but tie callers to the markup. Use domain callbacks for app components and DOM-style for primitives. - ↔
One generic change handler with
[name]: valuesaves code but loses per-field type checking (the computed key is astring). For a few fields, separate handlers or a reducer keep the types exact.
Done when you can
I let inline handlers be typed by context and hover to see their types.
I can annotate separate handlers with
ChangeEvent,MouseEvent,KeyboardEventandFormEvent(orSubmitEvent).I import event types from
react, not the DOM globals.I read values from
currentTargetand know whytargetis vague.I can type handler props as domain callbacks or with
MouseEventHandler/ComponentProps<"button">["onClick"].