Tipando event handlers
TL;DR
Eventos React são synthetic (wrappers em torno do DOM nativo). Os tipos vivem em
React.MouseEvent<T>,React.ChangeEvent<T>,React.FormEvent<T>, etc. O genéricoTé o elemento HTML que originou o evento (HTMLButtonElement,HTMLInputElement…). Cuidado comcurrentTarget(o elemento que tem o listener — sempreT) vstarget(qualquer node, pode ser child).
O que é
Eventos em React não são os mesmos eventos DOM nativos que você manipula com addEventListener. O React intercepta a árvore inteira no root e expõe os handlers via JSX (onClick, onChange, onSubmit…) através de um sistema próprio chamado synthetic events. Cada evento sintético é um wrapper sobre o evento DOM original, com a mesma API conceitual (preventDefault, stopPropagation, target, currentTarget) e algumas garantias adicionais — principalmente compatibilidade cross-browser, normalização de campos e integração com o ciclo de render.
A distinção operacional importa porque os tipos TypeScript são diferentes em cada universo:
- Em props JSX (
onClick={...},onChange={...}) — você recebe umReact.SyntheticEvent(ou um descendente especializado:MouseEvent,ChangeEvent,FormEvent,KeyboardEvent,FocusEvent,PointerEvent, etc). Todos vivem no namespaceReact.. - Em listeners imperativos (
window.addEventListener('mousemove', ...), dentro de umuseEffect) — você recebe o evento DOM nativo:MouseEventglobal,KeyboardEventglobal,FocusEventglobal. Sem prefixoReact.. São tipos distintos nolib.dom.d.tsdo TS.
Os synthetic events especializados são genéricos no elemento que originou o handler: React.MouseEvent<HTMLButtonElement>, React.ChangeEvent<HTMLInputElement>, React.FormEvent<HTMLFormElement>. Esse generic T determina o tipo de currentTarget (sempre T) e, em alguns casos, o tipo das props específicas do evento — em ChangeEvent<HTMLInputElement>, por exemplo, e.target.value existe porque o target foi estreitado para HTMLInputElement. Trocar o generic por outro elemento (ChangeEvent<HTMLDivElement>) faz e.target.value deixar de existir, porque <div> não tem value.
Por que importa
Tipar eventos errado é uma das fontes mais comuns de bugs sutis em React+TS, principalmente porque o sintoma quase sempre é runtime, não compile-time. Três armadilhas recorrentes ilustram o custo:
A primeira é acessar e.target.value em handlers que não são de input. Um onClick em <div> recebe React.MouseEvent<HTMLDivElement>, e e.target é EventTarget genérico — não tem .value. Sem tipagem, o dev escreve (e) => console.log(e.target.value) e em runtime recebe undefined. Com tipagem correta, o TS recusa: Property 'value' does not exist on type 'EventTarget'. O erro vira instantâneo em vez de virar suporte.
A segunda é confundir target com currentTarget. currentTarget é o elemento que tem o listener — sempre tipado como o generic T. target é o elemento concreto onde o evento aconteceu, que pode ser qualquer descendente (event bubbling). Em uma <ul onClick> com <li> filhos, clicar num <li> faz e.target ser HTMLLIElement e e.currentTarget ser HTMLUListElement. Tratar os dois como se fossem a mesma coisa quebra event delegation, padrão comum no ecossistema.
A terceira é cair no (e: any) => quando o handler “fica complicado”. O any apaga toda a segurança que o React+TS oferece — preventDefault em evento sem default, leitura de campos inexistentes, tudo passa silenciosamente. Pior: os autocompletes do editor desaparecem, e o dev passa a copiar nomes de propriedades de cabeça. A regra prática é não usar any em handlers — se o tipo correto é difícil de descobrir, hover no onClick da JSX revela a assinatura esperada.
Como funciona
Sample 1 — onClick em <button>
function Button() {
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
console.log(e.currentTarget); // HTMLButtonElement
};
return <button onClick={onClick}>OK</button>;
}React.MouseEvent<HTMLButtonElement> é o tipo canônico para onClick em <button>. O generic <HTMLButtonElement> informa ao TS que e.currentTarget é o próprio botão — útil quando o handler precisa ler disabled, form, dataset ou outros atributos do elemento. e.preventDefault() cancela o comportamento default (relevante quando o botão está dentro de <form> e o tipo default é submit).
Sample 2 — onChange em <input> text
function TextInput() {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // string — disponível em <input>
};
return <input type="text" onChange={onChange} />;
}Em ChangeEvent<HTMLInputElement>, o target é estreitado para HTMLInputElement, e portanto .value é acessível e tipado como string. Esse é um dos poucos casos onde usar e.target é seguro e idiomático — o generic já garantiu que o target é o elemento esperado. Para <select>, troque por HTMLSelectElement; para <textarea>, HTMLTextAreaElement.
Sample 3 — onSubmit em <form>
function MyForm() {
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// currentTarget é HTMLFormElement — sempre
const email = formData.get('email');
console.log(email);
};
return (
<form onSubmit={onSubmit}>
<input name="email" type="email" />
<button type="submit">Enviar</button>
</form>
);
}FormEvent<HTMLFormElement> é o tipo canônico para onSubmit. e.preventDefault() é quase sempre obrigatório — sem ele, o browser tenta fazer submit nativo (recarrega a página). new FormData(e.currentTarget) é o pattern idiomático para extrair valores de forma genérica: o currentTarget é tipado como HTMLFormElement, e FormData aceita exatamente esse tipo. Usar e.target ali daria erro de tipo (EventTarget não é assinable a HTMLFormElement sem narrowing).
Sample 4 — currentTarget vs target (a confusão clássica)
function ListClickable() {
const onClick = (e: React.MouseEvent<HTMLUListElement>) => {
// currentTarget: HTMLUListElement (o <ul> — onde o listener está)
console.log(e.currentTarget.tagName); // "UL"
// target: EventTarget — qualquer node descendente que recebeu o clique
// Não é tipado com T; é genérico.
if (e.target instanceof HTMLLIElement) {
console.log(e.target.dataset.id); // narrow primeiro
}
};
return (
<ul onClick={onClick}>
<li data-id="1">A</li>
<li data-id="2">B</li>
</ul>
);
}Esse é o pattern canônico de event delegation: um único listener no <ul> cobre todos os filhos <li>. currentTarget é sempre o <ul> (onde o handler está registrado), enquanto target é o elemento concreto que recebeu o clique — pode ser um <li>, ou um nó de texto dentro dele. O TS força narrowing: e.target instanceof HTMLLIElement estreita o tipo para HTMLLIElement no bloco do if, e só então dataset.id é acessível. Sem o narrow, e.target.dataset falha com Property 'dataset' does not exist on type 'EventTarget'.
Sample 5 — Handler reutilizável tipado
import { useState } from 'react';
// Handler genérico para forms — adiciona name/value ao state
function makeChangeHandler<T extends Record<string, unknown>>(
setState: React.Dispatch<React.SetStateAction<T>>
) {
return (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setState(prev => ({ ...prev, [name]: value }));
};
}
// Uso:
function ProfileForm() {
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = makeChangeHandler(setForm);
return (
<form>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
</form>
);
}A factory makeChangeHandler recebe o setState do estado-objeto e devolve um onChange reutilizável que distribui o valor pelo campo nomeado. O constraint T extends Record<string, unknown> garante que o estado é um objeto indexável por string, condição para o spread { ...prev, [name]: value } ser seguro. React.Dispatch<React.SetStateAction<T>> é o tipo canônico do segundo elemento retornado por useState<T> — usar esse tipo na assinatura comunica claramente que o handler espera o setter, não o valor. Essa fábrica é o esqueleto de patterns mais sofisticados em libs como Formik e React Hook Form, cobertos em nota 10.
Sample 6 — Listener nativo (em useEffect com addEventListener)
import { useEffect } from 'react';
function MouseTracker() {
useEffect(() => {
// Aqui é DOM nativo, não synthetic — usa MouseEvent direto, sem React.
const handler = (e: MouseEvent) => {
console.log(e.clientX, e.clientY);
};
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return <div>move o mouse</div>;
}Quando o handler é registrado via addEventListener (na window, em refs DOM, em document), o tipo do evento é o DOM nativo — MouseEvent sem prefixo, vindo do lib.dom.d.ts. Os campos são parecidos (clientX, clientY, target, currentTarget), mas é um tipo diferente de React.MouseEvent. Misturar os dois (passar um React.MouseEvent para addEventListener ou vice-versa) dá erro de tipo. A regra mnemônica: se o handler chega via prop JSX, é synthetic; se chega via addEventListener, é nativo.
Sample 7 — KeyboardEvent em input
function SearchBox({ onSubmit }: { onSubmit: () => void }) {
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
onSubmit();
}
};
return <input type="search" onKeyDown={onKeyDown} />;
}KeyboardEvent<HTMLInputElement> cobre onKeyDown, onKeyUp, onKeyPress (deprecated, evite). O campo e.key é a string da tecla ('Enter', 'Escape', 'a'); e.code é o código físico da tecla no teclado ('KeyA', 'Enter', 'Escape'), independente do layout. Para shortcuts, e.key é o caminho idiomático.
Na prática
Pattern dominante no ecossistema é o handler único que escuta todos os campos de um form e despacha pelo name. A versão minimalista cabe numa linha:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setForm(f => ({ ...f, [e.target.name]: e.target.value }));Aplicado num form-objeto:
const [form, setForm] = useState({ name: '', email: '', age: '' });
return (
<>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input name="age" value={form.age} onChange={handleChange} />
</>
);Funciona porque e.target.name é a string do atributo name="..." do <input>, e e.target.value é o valor atual. O spread { ...f, [e.target.name]: e.target.value } substitui apenas a chave correspondente, mantendo as outras intactas. É suficiente para forms simples, mas tem limites: validação inline, transformação de tipos (string → number), feedback granular por campo — tudo isso é onde libs como React Hook Form + Zod entram. Aprofundamento em nota 10.
Armadilhas
-
Acessar
e.target.valueemonClick. OtargetemMouseEvent<HTMLButtonElement>(ou em qualquer mouse event) éEventTargetgenérico — não tem.value. O TS rejeita corretamente, mas em código comanyou type assertion abusiva o erro viraundefinedem runtime. A propriedade.valuesó está emChangeEvent<HTMLInputElement>(e similares para select/textarea), porque ali o target foi estreitado. Para ler valores em handlers de mouse, usee.currentTarget(que é tipado como o genericT) ou puxe o valor via ref. -
Confundir
MouseEvent(DOM nativo) comReact.MouseEvent(synthetic). Em listeners imperativos (addEventListener,removeEventListener), o tipo é o nativo (MouseEventsem prefixo). Em props JSX (onClick,onMouseMove), é o synthetic (React.MouseEvent). São tipos diferentes — passar um por outro causa erros de assignability tipoType 'MouseEvent' is not assignable to type 'React.MouseEvent<HTMLDivElement>'. A pista mnemônica: se você vêaddEventListenerperto, é nativo. -
Usar
anyem handlers. Sintoma:(e: any) => { ... }. Custo: TS perde toda a checagem, autocomplete some, propriedades inexistentes passam silenciosamente. Quando o tipo certo é difícil de adivinhar, hover na prop JSX (onClick) no editor revela a assinatura esperada. Última opção éReact.SyntheticEvent(o pai de todos), que pelo menos preservapreventDefault,stopPropagation,targetecurrentTargetgenericamente. -
Esquecer o generic em
React.ChangeEvent(ou em qualquer event tipado). Sem<HTMLInputElement>,e.targetviraEventTargetgenérico e.valuedesaparece. O TS reclama comProperty 'value' does not exist on type 'EventTarget'. A correção é sempre passar o generic do elemento que originou o handler:React.ChangeEvent<HTMLInputElement>,React.MouseEvent<HTMLButtonElement>,React.FormEvent<HTMLFormElement>. -
Tipar
e.targetcomo o elemento, em vez de narrow viainstanceof. Em casos de event delegation (handler num pai, clique vem de filho),e.targeté genuinamenteEventTarget— não dá pra estreitar via generic do evento, porque o pai não sabe quem é o filho que vai disparar. A solução idiomática é narrow runtime:if (e.target instanceof HTMLLIElement) { ... }. Forçar comas HTMLLIElementmente para o TS e quebra silenciosamente quando o clique vem de outro descendente.
Em entrevista
“React events are synthetic — wrappers around the native DOM events. The TypeScript types live in
React.MouseEvent<T>,React.ChangeEvent<T>,React.FormEvent<T>, and so on. The genericTis the HTML element that originated the event — it determines whatcurrentTargetis. The most common mistake is confusingcurrentTargetwithtarget:currentTargetis the element that has the listener (always typed asT), whiletargetcan be any descendant element that bubbled. For native listeners added viaaddEventListenerin auseEffect, you use the DOM native types —MouseEventwithout theReact.prefix.”
Vocabulário-chave: synthetic event, native event, currentTarget, target, event bubbling, event delegation.
Pergunta típica de senior interview: “What’s the difference between e.target and e.currentTarget in a React event handler?” — resposta defensiva: currentTarget is the element the handler is attached to — it’s typed as the generic T you passed to React.MouseEvent<T> or similar, so you always know its concrete type. target is the actual element where the event originated, which can be any descendant due to event bubbling. It’s typed as EventTarget and requires runtime narrowing — typically instanceof HTMLLIElement or similar — before you can access element-specific properties. Event delegation patterns rely on this distinction: one listener on the parent, runtime narrowing to figure out which child triggered.
Veja também
- 05 - Tipando state e refs
- 10 - Tipando formulários
- React — seção “Hooks essenciais” (eventos em handlers)