Typing React Events
Correct event types for onClick, onChange, forms, and keyboard handlers in React TypeScript — synthetic events and target narrowing.
- typescript
- typing-react
React’s event system uses SyntheticEvent wrappers. Under TypeScript, each handler prop expects a specific event type. Getting this wrong leads to any, useless event.target, or fights with value on inputs.
Docs: React — Responding to Events, @types/react event definitions.
Common handler types
import type { ChangeEvent, FormEvent, KeyboardEvent, MouseEvent } from 'react';
function Form() {
function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
}
function onChange(e: ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
function onClick(e: MouseEvent<HTMLButtonElement>) {
console.log(e.currentTarget.name);
}
function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') e.currentTarget.blur();
}
return (
<form onSubmit={onSubmit}>
<input name="email" onChange={onChange} onKeyDown={onKeyDown} />
<button type="submit" name="save" onClick={onClick}>
Save
</button>
</form>
);
}
Generic parameter is the element type the handler is attached to.
target vs currentTarget
currentTarget— element the handler is bound to (typed from the generic).target— event origin (may be a child); typed more loosely (EventTarget).
function onClick(e: MouseEvent<HTMLButtonElement>) {
// e.currentTarget is HTMLButtonElement
// e.target might be a nested <span>
}
For inputs, ChangeEvent<HTMLInputElement> makes e.target an HTMLInputElement.
Inline props inference
<button
type="button"
onClick={(e) => {
// e inferred as MouseEvent<HTMLButtonElement>
}}
/>
When extracting handlers, annotate explicitly if inference breaks.
Textarea and select
function onArea(e: ChangeEvent<HTMLTextAreaElement>) {
setText(e.target.value);
}
function onSelect(e: ChangeEvent<HTMLSelectElement>) {
setValue(e.target.value);
}
Don’t type everything as ChangeEvent<HTMLInputElement>.
Checkbox / radio
function onToggle(e: ChangeEvent<HTMLInputElement>) {
setChecked(e.target.checked);
}
Drag and clipboard (sketch)
function onDragStart(e: React.DragEvent<HTMLDivElement>) {
e.dataTransfer.setData('text/plain', id);
}
function onPaste(e: React.ClipboardEvent<HTMLInputElement>) {
const text = e.clipboardData.getData('text');
}
Avoid any and bare Event
// Weak
function onClick(e: any) {}
function onClick(e: Event) {
// e.currentTarget not an HTMLElement without narrowing
}
Use React’s event types from @types/react.
Native vs synthetic
In portals or non-React listeners you use DOM types (MouseEvent from lib.dom). Don’t mix freely without care — property names are similar but types differ.
Footguns
- Wrong element generic —
MouseEvent<HTMLDivElement>on a button handler. - Using
e.target.valueon non-input — narrow or usecurrentTarget. - Forgetting
preventDefaulttyping — still on SyntheticEvent. - Legacy
React.MouseEventnamespace — fine; prefer direct type imports in modern code.
Interview out-loud answer
“I type React handlers with MouseEvent, ChangeEvent, FormEvent, parameterized by the element. I prefer currentTarget for the bound element and the correct input variant for value/checked. Inline handlers often infer; extracted ones get explicit annotations.”
Generic event handlers
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
setValue(e.target.value);
}
Union element types when one handler serves multiple fields. For currentTarget, the generic parameter should match the element you attach the handler to.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Notes from real codebases
Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.
Related on this site
- Typing React props
- Type guards typeof and instanceof
- Type narrowing
- Accessible forms errors
- user-event vs fireEvent
Further reading
Related guides
- Typing React PropsType component props with required/optional fields, children, unions for variants, and HTML attribute extension patterns.
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.