Tipando eventos do DOM
1. Fundamentos dos Tipos de Evento no DOM
1.1. A interface Event e suas propriedades básicas
Todo evento no DOM herda da interface base Event, que fornece propriedades fundamentais como type (string que identifica o evento), target (o elemento que disparou o evento) e currentTarget (o elemento ao qual o handler está vinculado). Em TypeScript, essas propriedades são tipadas de forma genérica:
document.addEventListener('click', (event: Event) => {
console.log(event.type); // "click"
console.log(event.target); // EventTarget | null
console.log(event.currentTarget); // EventTarget | null
});
1.2. Hierarquia de tipos de evento
TypeScript modela fielmente a hierarquia de eventos do DOM. Abaixo da interface Event, temos subtipos especializados:
UIEvent— eventos de interface do usuário (herda deEvent)MouseEvent— eventos de mouse (herda deUIEvent)KeyboardEvent— eventos de teclado (herda deUIEvent)FocusEvent— eventos de foco (herda deUIEvent)ClipboardEvent— eventos de área de transferência (herda deEvent)
// MouseEvent adiciona propriedades como clientX, clientY, button
document.addEventListener('mousedown', (event: MouseEvent) => {
console.log(`Clique em (${event.clientX}, ${event.clientY})`);
});
// KeyboardEvent adiciona key, code, ctrlKey
document.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter' && event.ctrlKey) {
console.log('Ctrl+Enter pressionado');
}
});
1.3. Diferença entre EventTarget e HTMLElement
Um ponto crucial de confusão: event.target é tipado como EventTarget | null, não como HTMLElement. Isso porque o target pode ser qualquer nó do DOM (incluindo Document, Window, ou Text nodes). Para acessar propriedades de elemento HTML, você precisa fazer narrowing:
document.querySelector('button')?.addEventListener('click', (event: Event) => {
const target = event.target as HTMLElement; // Casting necessário
target.style.backgroundColor = 'red';
});
2. Tipando Event Handlers Inline e com addEventListener
2.1. Tipagem implícita vs explícita
Quando você usa addEventListener, o TypeScript infere automaticamente o tipo do evento com base no nome do evento:
// TypeScript infere: event: MouseEvent
document.addEventListener('click', (event) => {
// event.clientX está disponível sem casting
});
// Para eventos menos comuns, a inferência pode ser genérica
document.addEventListener('customEvent', (event) => {
// event é tipado como Event (genérico)
});
2.2. Uso de (event: Event) vs tipos específicos
Sempre que possível, use o tipo específico para acessar propriedades especializadas:
// Ruim: perde acesso a propriedades específicas
document.addEventListener('keydown', (event: Event) => {
// event.key não existe em Event
});
// Bom: KeyboardEvent expõe key, code, etc.
document.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Tecla ESC pressionada');
}
});
2.3. Casting seguro com as e type guards
Para acessar target corretamente, combine casting com verificações:
document.querySelector('input')?.addEventListener('input', (event: Event) => {
const target = event.target as HTMLInputElement;
console.log(target.value); // Agora é seguro
// Alternativa com type guard
if (event.target instanceof HTMLInputElement) {
console.log(event.target.value);
}
});
3. Acessando Propriedades Específicas de Cada Tipo de Evento
3.1. MouseEvent
element.addEventListener('mousemove', (event: MouseEvent) => {
const { clientX, clientY, button, relatedTarget } = event;
console.log(`Mouse em (${clientX}, ${clientY}), botão: ${button}`);
// relatedTarget é o elemento de onde o mouse veio (mouseover) ou para onde foi (mouseout)
if (relatedTarget instanceof HTMLElement) {
console.log('Relacionado a:', relatedTarget.tagName);
}
});
3.2. KeyboardEvent
document.addEventListener('keydown', (event: KeyboardEvent) => {
// key: valor da tecla (ex: "a", "Enter", "ArrowUp")
// code: código físico da tecla (ex: "KeyA", "Enter", "ArrowUp")
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault(); // Impede ação padrão
console.log('Shift+Enter detectado');
}
});
3.3. FocusEvent
input.addEventListener('focus', (event: FocusEvent) => {
// relatedTarget: elemento que perdeu o foco (se houver)
const previousElement = event.relatedTarget as HTMLElement | null;
if (previousElement) {
console.log('Foco veio de:', previousElement.tagName);
}
});
input.addEventListener('blur', (event: FocusEvent) => {
// relatedTarget agora é o elemento que recebeu o foco
});
4. Tipando Eventos Customizados e Event Delegation
4.1. Criando e tipando eventos customizados com CustomEvent<T>
// Definindo um evento customizado tipado
interface UserData {
id: number;
name: string;
}
// Disparando o evento
const userEvent = new CustomEvent<UserData>('user-login', {
detail: { id: 1, name: 'Alice' }
});
window.dispatchEvent(userEvent);
// Ouvindo com tipo correto
window.addEventListener('user-login', (event: CustomEvent<UserData>) => {
console.log(event.detail.name); // "Alice" — totalmente tipado
});
4.2. Tipagem segura em event delegation
const container = document.getElementById('list-container')!;
container.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Verificação segura com instanceof
if (target instanceof HTMLButtonElement && target.dataset.action === 'delete') {
console.log('Deletar item:', target.dataset.id);
}
// Ou use matches para seletores CSS
if (target.matches('li.item')) {
console.log('Item clicado:', target.textContent);
}
});
4.3. Type predicates para narrowing
function isHTMLElement(target: EventTarget | null): target is HTMLElement {
return target instanceof HTMLElement;
}
function isInputElement(target: EventTarget | null): target is HTMLInputElement {
return target instanceof HTMLInputElement;
}
document.addEventListener('click', (event: MouseEvent) => {
if (isInputElement(event.target)) {
console.log('Input clicado:', event.target.value);
} else if (isHTMLElement(event.target)) {
console.log('Elemento HTML clicado:', event.target.tagName);
}
});
5. Event Handlers em Componentes React com TypeScript
5.1. Tipos nativos do React
React possui seus próprios tipos de evento que encapsulam os eventos nativos do DOM:
import React from 'react';
function Button() {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log(event.clientX, event.clientY);
// event.target é HTMLButtonElement
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
return (
<>
<button onClick={handleClick}>Clique</button>
<input onChange={handleChange} />
</>
);
}
5.2. Diferença entre SyntheticEvent e eventos nativos
React usa SyntheticEvent, um wrapper que normaliza eventos entre navegadores. Ele possui as mesmas propriedades que os eventos nativos, mas não é exatamente o mesmo objeto:
function Input() {
const handleNative = (event: React.SyntheticEvent<HTMLInputElement>) => {
// SyntheticEvent tem preventDefault(), stopPropagation(), etc.
event.preventDefault();
// Para acessar o evento nativo:
const nativeEvent = event.nativeEvent; // Event (nativo do DOM)
};
}
5.3. Genéricos em handlers
interface SelectProps<T extends HTMLElement> {
onChange: (e: React.ChangeEvent<T>) => void;
}
function Select({ onChange }: SelectProps<HTMLSelectElement>) {
return (
<select onChange={onChange}>
<option value="1">Opção 1</option>
</select>
);
}
6. Padrões Avançados e Boas Práticas
6.1. Criando tipos utilitários
type TypedEventHandler<T extends HTMLElement, E extends Event> =
(event: E & { target: T }) => void;
// Uso:
const handleClick: TypedEventHandler<HTMLButtonElement, MouseEvent> = (event) => {
event.target.disabled = true; // target é HTMLButtonElement
};
6.2. Sobrecarga de funções para múltiplos tipos de evento
function handleEvent(event: MouseEvent): void;
function handleEvent(event: KeyboardEvent): void;
function handleEvent(event: FocusEvent): void;
function handleEvent(event: Event): void {
if (event instanceof MouseEvent) {
console.log('Mouse:', event.clientX);
} else if (event instanceof KeyboardEvent) {
console.log('Tecla:', event.key);
} else if (event instanceof FocusEvent) {
console.log('Foco');
}
}
6.3. Evitando any: uso de EventMap e keyof HTMLElementEventMap
// HTMLElementEventMap mapeia nomes de eventos para seus tipos
type EventName = keyof HTMLElementEventMap;
function addTypedListener<K extends EventName>(
element: HTMLElement,
eventName: K,
handler: (event: HTMLElementEventMap[K]) => void
) {
element.addEventListener(eventName, handler as EventListener);
}
// Uso com inferência automática
addTypedListener(document.body, 'click', (event) => {
// event é MouseEvent
console.log(event.clientX);
});
Referências
- MDN Web Docs: Event — Documentação completa da interface Event base do DOM, com todas as propriedades e métodos.
- TypeScript Handbook: DOM Manipulation — Guia oficial do TypeScript sobre manipulação do DOM, incluindo tipagem de eventos.
- React TypeScript Cheatsheet: Event Handling — Referência completa para tipagem de eventos em React com TypeScript.
- TypeScript: Utility Types for Event Handlers — Artigo técnico sobre criação de handlers de eventos type-safe com tipos utilitários.
- CSS-Tricks: Event Delegation in TypeScript — Tutorial prático sobre event delegation com tipagem segura em TypeScript.