Alright, let’s talk about events. You’ve handled events in vanilla JavaScript: a simple onclick here, an onchange there. It’s a straightforward, if slightly clunky, affair. Then you waltz into the React ballroom, and suddenly you’re expected to dance with React.MouseEvent<HTMLButtonElement>. It feels like your plain old Event object got a fancy, overly verbose makeover. Why the ceremony?

It boils down to one beautiful, non-negotiable TypeScript truth: type safety. React wraps the browser’s native event object into its own synthetic event system for performance and consistency. Our job is to tell TypeScript exactly what kind of event we’re handling and, crucially, which HTML element is producing it. This stops you from trying to get .value from a <div> (which would be embarrassing) or .checked from a <select> (which is just nonsense). We’re not just preventing runtime errors; we’re designing our component contracts to be unbreakable.

The Core Pattern: React.SomethingEvent<ElementType>

The syntax is consistent, even if it looks intimidating at first. You’ll almost always be dealing with the React.*Event namespace. The generic type parameter—the bit inside the angle brackets <>—is where you specify the DOM element that will dispatch the event. This is the magic key that unlocks full type safety.

// This is the classic. You're handling a click on a button. So you use...
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  event.preventDefault(); // Stop that button from submitting a form, maybe.
  console.log('Clicked! Client coordinates:', event.clientX, event.clientY);
};

// Usage
<button onClick={handleClick}>Click Me!</button>

See? React.MouseEvent for the type of event, HTMLButtonElement for the source. This tells TypeScript that event.target is a button, and we can access all the properties a mouse event on a button would have.

The Usual Suspects: ChangeEvent and FormEvent

While MouseEvent is the poster child, ChangeEvent is the workhorse of forms. It’s primarily used for inputs, textareas, and selects.

// For a standard text input
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  console.log('New value:', event.target.value); // .value is now a string
};

// For a checkbox? Different element, same event type.
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  console.log('Checked?', event.target.checked); // .checked is now a boolean
};

<input type="text" onChange={handleInputChange} />
<input type="checkbox" onChange={handleCheckboxChange} />

Notice we’re still using HTMLInputElement for the checkbox. That’s because both a text input and a checkbox are, fundamentally, <input> elements. Their value and checked properties are differentiated by the runtime type, but their base type is the same.

Then there’s FormEvent, which is perfect for handling the form’s overall onSubmit. This is often better than putting a click handler on the submit button.

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault(); // Absolutely critical. Otherwise, the page reloads.
  // Do your data processing here, like reading from your state.
};

<form onSubmit={handleSubmit}>
  ...your inputs...
  <button type="submit">Submit</button>
</form>

The Inline Arrow Function Shortcut

You will see this everywhere, and for good reason: it’s concise and scopes the handler perfectly. Typing it inline can be confusing at first. The trick is to let your IDE’s autocomplete do the work.

<input
  type="text"
  onChange={(event) => { // Hover over 'event'. Your IDE should show you: React.ChangeEvent<HTMLInputElement>
    setState(event.target.value);
  }}
/>

If you’re using a linter like ESLint, it might scream at you for defining a function inside the prop like this on every render (for performance reasons). For simple cases, it’s fine. For complex logic, pull it out into a useCallback as we did earlier.

The Pitfall: Not Typing the Event Parameter

The biggest mistake is just using any or, worse, leaving the event parameter untyped. Don’t do it. You’re throwing away the entire reason you’re using TypeScript.

// ❌ The Dark Path (Don't do this)
const badHandler = (event: any) => {
  console.log(event.target.whatever); // This compiles. It will explode at runtime.
};

// ✅ The Way of the Type-Safe Ninja
const goodHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
  console.log(event.target.value); // This is guaranteed to work.
};

When You Need the Native Event

Very, very rarely, you might need to access a property that exists on the native browser event but isn’t present on React’s synthetic event. React’s event system is a wrapper, and it pools events for performance. If you need something from the native event, use event.nativeEvent.

const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
  // Need a property only on the native MouseEvent?
  const nativeEventType = event.nativeEvent.type;
  console.log(nativeEventType); // 'mousemove'
};

You’ll almost never need this, but it’s good to know it’s there. Consider it an escape hatch for when React’s abstraction is, for once, slightly too abstract. Now you’re armed. Go forth and handle events without fear.