How to type event handlers.
Every event, one cheat sheet.
React ships types for every DOM event — ChangeEvent, MouseEvent, FormEvent, KeyboardEvent, FocusEvent, and more. Each is a generic over the element type. Get the type right and e.target is properly typed with .value, .checked, .files, etc. Get it wrong and everything becomes any. This page is the complete reference.
Short answer: Use React.ChangeEvent for input changes, React.FormEvent for submits, React.MouseEvent for clicks, React.KeyboardEvent for key presses, React.FocusEvent for focus/blur. The element type is the element the handler attaches to. Inline handlers infer automatically — only named functions need annotations. Always prefer e.currentTarget over e.target for form/container events.
✅ Solution 1 — The event type cheat sheet
Every common event, mapped to its TypeScript type.
These are the types you'll reach for daily. Save this list — after a week, you'll recognize the pattern: React.[EventName]Event<[HTMLElement]>. The element generic is the type of the element you're attaching to.
import { useState } from 'react'; function AllEvents() { const [text, setText] = useState(''); const [checked, setChecked] = useState(false); const [selected, setSelected] = useState('a'); // ─── Change — text input ───────────────────── function onTextChange(e: React.ChangeEvent) { setText(e.target.value); // string } // ─── Change — textarea ─────────────────────── function onTextareaChange(e: React.ChangeEvent ) { setText(e.target.value); } // ─── Change — select ────────────────��──────── function onSelectChange(e: React.ChangeEvent ) { setSelected(e.target.value); } // ─── Change — checkbox ─────────────────────── function onCheckChange(e: React.ChangeEvent ) { setChecked(e.target.checked); // boolean } // ─── Change — file input ───────────────────── function onFileChange(e: React.ChangeEvent ) { const files = e.target.files; // FileList | null if (files && files.length > 0) { console.log(files[0].name); } } // ─── Submit — form ─────────────────────────── function onSubmit(e: React.FormEvent ) { e.preventDefault(); const form = e.currentTarget; console.log(new FormData(form)); } // ─── Click — button ────────────────────────── function onClick(e: React.MouseEvent ) { console.log('clicked at', e.clientX, e.clientY); console.log('button disabled?', e.currentTarget.disabled); } // ─── Keyboard — input ──────────────────────── function onKeyDown(e: React.KeyboardEvent ) { if (e.key === 'Enter') console.log('submitted'); if (e.ctrlKey && e.key === 's') { e.preventDefault(); console.log('saved'); } } // ─── Focus / Blur ──────────────────────────── function onFocus(e: React.FocusEvent ) { e.target.select(); } // ─── The complete cheat sheet ──────────────── // // onChange → React.ChangeEvent // (or HTMLTextAreaElement, HTMLSelectElement) // onSubmit → React.FormEvent return ( ); } export default AllEvents;// onClick → React.MouseEvent // (or HTMLDivElement, HTMLAnchorElement) // onKeyDown → React.KeyboardEvent // onKeyUp → React.KeyboardEvent // onKeyPress → React.KeyboardEvent // onFocus → React.FocusEvent // onBlur → React.FocusEvent // onScroll → React.UIEvent // onMouseEnter → React.MouseEvent // onMouseLeave → React.MouseEvent // onDrag → React.DragEvent // onDrop → React.DragEvent // onWheel → React.WheelEvent // onPointer → React.PointerEvent // onTouch → React.TouchEvent // onAnimation → React.AnimationEvent // onTransition → React.TransitionEvent
import { useState } from 'react'; function AllEvents() { const [text, setText] = useState(''); const [checked, setChecked] = useState(false); const [selected, setSelected] = useState('a'); function onTextChange(e) { setText(e.target.value); } function onTextareaChange(e) { setText(e.target.value); } function onSelectChange(e) { setSelected(e.target.value); } function onCheckChange(e) { setChecked(e.target.checked); } function onFileChange(e) { const files = e.target.files; if (files && files.length > 0) { console.log(files[0].name); } } function onSubmit(e) { e.preventDefault(); const form = e.currentTarget; console.log(new FormData(form)); } function onClick(e) { console.log('clicked at', e.clientX, e.clientY); console.log('button disabled?', e.currentTarget.disabled); } function onKeyDown(e) { if (e.key === 'Enter') console.log('submitted'); if (e.ctrlKey && e.key === 's') { e.preventDefault(); console.log('saved'); } } function onFocus(e) { e.target.select(); } return React.createElement('form', { onSubmit }, React.createElement('input', { value: text, onChange: onTextChange, onKeyDown, onFocus }), React.createElement('textarea', { value: text, onChange: onTextareaChange }), React.createElement('select', { value: selected, onChange: onSelectChange }, React.createElement('option', { value: 'a' }, 'A'), React.createElement('option', { value: 'b' }, 'B') ), React.createElement('input', { type: 'checkbox', checked, onChange: onCheckChange }), React.createElement('input', { type: 'file', onChange: onFileChange }), React.createElement('button', { type: 'submit', onClick }, 'Submit') ); } export default AllEvents;
The pattern
React.[Event] — every event follows this. The element type matches what you attach the handler to.
Element matters for target
HTMLInputElement gives e.target.value, .checked, .files. HTMLSelectElement gives .selectedOptions.
Inline handlers infer
onClick={(e) => ...} — e is inferred. Only named functions need annotations.
Event type hierarchy
All events extend React.SyntheticEvent. Specific events (ChangeEvent, MouseEvent) add their properties. Use the specific type.
Match the element generic to the element, not to the value type. A checkbox's onChange is ChangeEvent — not ChangeEvent (which doesn't exist). A is HTMLTextAreaElement, not HTMLInputElement. When in doubt, hover over the JSX attribute in VS Code — the inferred type shows the exact event type React expects. Copy it, then annotate your named function with the same type.
✅ Solution 2 — target vs currentTarget
The subtle bug that half of React devs hit.
Both are typed as the element generic, but they mean different things. e.target is the element that dispatched the event — could be a child if the event bubbled. e.currentTarget is the element the handler is attached to. For container events (forms, modals, delegated handlers), currentTarget is what you want.
import { useState } from 'react'; function FormExample() { const [values, setValues] = useState({ name: '', email: '' }); // ─── Form submit — use currentTarget ───────── function onSubmit(e: React.FormEvent) { e.preventDefault(); // ✅ currentTarget — the form itself const form = e.currentTarget; const data = new FormData(form); console.log(Object.fromEntries(data)); // ⚠️ target — could be the submit button or an input // that triggered a change event bubbling up. // new FormData(e.target as HTMLFormElement) — sometimes // works, sometimes throws depending on what bubbled. } // ─── Change handler — target is fine ───────── function onNameChange(e: React.ChangeEvent ) { // ✅ target here IS the input (change events // don't bubble from children of the input) setValues(v => ({ ...v, name: e.target.value })); } // ─── Container click delegation ────────────── function onContainerClick(e: React.MouseEvent ) { // ✅ currentTarget — the div // ⚠️ target — whatever was clicked (could be a child) console.log('container clicked'); console.log('container:', e.currentTarget.className); console.log('clicked child:', (e.target as HTMLElement).className); // Common pattern: close a modal/dropdown // when the backdrop is clicked but not the content. if (e.target === e.currentTarget) { console.log('clicked backdrop, not content'); } } // ─── The rule ──────────────────────────────── // // Use e.currentTarget when: // - Form submit (need the whole form) // - Container click (need to distinguish backdrop) // - Any event that bubbles from children // - Any time you need "the element with the handler" // // Use e.target when: // - Change events on inputs (target is always the input) // - You genuinely want "the element that fired the event" // - You're handling event delegation intentionally return ( value={values.name} onChange={onNameChange} /> "email" value={values.email} onChange={e => setValues(v => ({ ...v, email: e.target.value }))} /> ); } export default FormExample;
import { useState } from 'react'; function FormExample() { const [values, setValues] = useState({ name: '', email: '' }); function onSubmit(e) { e.preventDefault(); const form = e.currentTarget; const data = new FormData(form); console.log(Object.fromEntries(data)); } function onNameChange(e) { setValues(v => ({ ...v, name: e.target.value })); } function onContainerClick(e) { console.log('container clicked'); console.log('container:', e.currentTarget.className); console.log('clicked child:', e.target.className); if (e.target === e.currentTarget) { console.log('clicked backdrop, not content'); } } return React.createElement('form', { onSubmit }, React.createElement('input', { name: 'name', value: values.name, onChange: onNameChange }), React.createElement('input', { name: 'email', value: values.email, onChange: e => setValues(v => ({ ...v, email: e.target.value })) }), React.createElement('button', { type: 'submit' }, 'Submit') ); } export default FormExample;
target = event source
The element that dispatched the event. For change events on inputs, this is the input. For container events, it might be a child.
currentTarget = handler owner
The element the handler is attached to. For form submit, this is always the form. For container click, this is the container.
The backdrop click pattern
if (e.target === e.currentTarget) — true only when the outer element itself was clicked, not a child. Classic for closing modals.
Change events are safe
Change events on inputs don't bubble from children — target and currentTarget are both the input. Either works.
Never cast e.target as HTMLFormElement to silence the compiler. If the event is a submit and you need the form, use e.currentTarget — it's guaranteed to be the form. Casting target works until a child triggers the event (a submit button, an enter key inside an input), at which point target is the button/input, not the form, and the cast lies. The type system is telling you something real — listen to it.
✅ Solution 3 — Type-safe handler types
Extract the event type when you don't want to hardcode it.
Sometimes you don't want to hardcode React.ChangeEvent. React ships utility types to extract the handler type from the element's props — React.ComponentProps, or the built-in aliases like ChangeHandler. These are useful for library code and wrapper components.
// ─── Extract the handler type from props ───── type InputChangeHandler = React.ComponentProps<'input'>['onChange']; // (event: React.ChangeEvent) => void const handleInput: InputChangeHandler = (e) => { console.log(e.target.value); }; // ─── Or use the built-in aliases ───────────── type ChangeHandler = React.ChangeEventHandler; type ClickHandler = React.MouseEventHandler ; type SubmitHandler = React.FormEventHandler ; type KeyHandler = React.KeyboardEventHandler ; const onChange: ChangeHandler = (e) => console.log(e.target.value); const onClick: ClickHandler = (e) => console.log(e.clientX); const onSubmit: SubmitHandler = (e) => e.preventDefault(); const onKeyDown: KeyHandler = (e) => console.log(e.key); // ─── Reusable handler factory ──────────────── function makeInputHandler( onValue: (value: string) => void ): React.ChangeEventHandler { return (e) => onValue(e.target.value); } const handleName = makeInputHandler(v => console.log('name:', v)); const handleEmail = makeInputHandler(v => console.log('email:', v)); // ─── Wrapper component typing ───────────────── interface ButtonProps { onClick?: React.MouseEventHandler ; children: React.ReactNode; } function Button({ onClick, children }: ButtonProps) { return ( ); } // ─── Generic event handler ─────────────────── function logEvent extends React.SyntheticEvent>(e: E, label: string) { e.preventDefault(); console.log(label, e.type, e.target); } // Works for any event: // logEvent(clickEvent, 'click'); // logEvent(submitEvent, 'submit'); // logEvent(changeEvent, 'change'); // ─── The types you'll use most often ───────── // // React.ChangeEventHandler = (e: ChangeEvent console.log(handleInput, onChange, onClick, onSubmit, onKeyDown, handleName, handleEmail);) => void // React.MouseEventHandler = (e: MouseEvent ) => void // React.FormEventHandler = (e: FormEvent ) => void // React.KeyboardEventHandler = (e: KeyboardEvent ) => void // React.FocusEventHandler = (e: FocusEvent ) => void // // These are the "handler versions" — use them for // prop types and named function annotations. Use the // "Event" versions (ChangeEvent, MouseEvent) when // you're writing the handler inline or extracting types.
const handleInput = (e) => { console.log(e.target.value); }; const onChange = (e) => console.log(e.target.value); const onClick = (e) => console.log(e.clientX); const onSubmit = (e) => e.preventDefault(); const onKeyDown = (e) => console.log(e.key); function makeInputHandler(onValue) { return (e) => onValue(e.target.value); } const handleName = makeInputHandler(v => console.log('name:', v)); const handleEmail = makeInputHandler(v => console.log('email:', v)); function Button({ onClick, children }) { return React.createElement('button', { onClick, className: 'btn' }, children); } function logEvent(e, label) { e.preventDefault(); console.log(label, e.type, e.target); } console.log(handleInput, onChange, onClick, onSubmit, onKeyDown, handleName, handleEmail);
ComponentProps
Extract the handler type from any element's props: React.ComponentProps<'input'>['onChange']. No hardcoding needed.
Built-in aliases
React.ChangeEventHandler = (e: ChangeEvent. Use these in prop types — they're shorter and clearer.
Handler factories
makeInputHandler(fn) returns a typed ChangeEventHandler. Reusable for any number of inputs.
Generic events
— a handler that works for any event. Use when the same logic applies across event types.
⚠️ Common mistakes
The event handler type bugs that cost real time.
any for events(e: any) => e.target.value — no type checking. Fix: (e: React.ChangeEvent.
ChangeEvent for a textarea — wrong. Fix: ChangeEvent. Match the actual element.
e.target for form submite.target as HTMLFormElement — cast is a lie if the button was clicked. Fix: e.currentTarget, already the form.
preventDefaultForm submits reload the page. Fix: e.preventDefault() at the top of the submit handler. TypeScript can't remind you.
onClick={(e: React.MouseEvent) => ...} — inference already handles it. Fix: only annotate named/extracted functions.
Event instead of React.SyntheticEvent(e: Event) — DOM Event, missing React properties. Fix: (e: React.SyntheticEvent) or the specific event type.
e.key on the wrong evente.key exists on KeyboardEvent, not ChangeEvent. Fix: use onKeyDown with React.KeyboardEvent.
e.target.filese.target.files[0] — files might be null. Fix: if (e.target.files) e.target.files[0], or e.target.files?.[0].
❓ Frequently asked
How do I type an onChange event in React + TypeScript?
Use React.ChangeEvent
What is the difference between e.target and e.currentTarget?
;e.target is the element that dispatched the event — could be a child element inside the handler's scope. e.currentTarget is the element the handler is attached to. For form handlers and delegated events, use e.currentTarget.
How do I type a form submit handler?
Use React.FormEvent
Why does my event handler need a type annotation?
Inline handlers in JSX get their types inferred from the element's props. But when you extract a handler to a named function and pass it as onClick={handleClick}, TypeScript can't infer the parameter type from the JSX attribute. You must annotate it manually.