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 de Event)
  • MouseEvent — eventos de mouse (herda de UIEvent)
  • KeyboardEvent — eventos de teclado (herda de UIEvent)
  • FocusEvent — eventos de foco (herda de UIEvent)
  • ClipboardEvent — eventos de área de transferência (herda de Event)
// 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