ESC

Type to search the knowledge base.

Typing React Events

Correct event types for onClick, onChange, forms, and keyboard handlers in React TypeScript — synthetic events and target narrowing.

intermediate3 min read
  • 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

  1. Wrong element generic — MouseEvent<HTMLDivElement> on a button handler.
  2. Using e.target.value on non-input — narrow or use currentTarget.
  3. Forgetting preventDefault typing — still on SyntheticEvent.
  4. Legacy React.MouseEvent namespace — 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.

Further reading

Related guides