CodeForge

TypeScript de Cero a Experto / TS en el mundo real

Tipar el DOM: querySelector, null y eventos

Teoría22 min20 XP

Bajamos de las alturas de los tipos avanzados al asfalto: el DOM. TypeScript conoce el DOM a fondo, pero eso significa que te OBLIGA a manejar dos verdades que en JavaScript ignorabas: que querySelector puede devolver null, y que cada elemento tiene su tipo específico. Hoy tipas la manipulación del DOM con honestidad —y el resultado es código sin los Cannot read properties of null de siempre.

El DOM ya viene tipado (y es honesto)

La verdad número uno: puede ser null

querySelector devuelve Element | null —porque el selector puede no encontrar nada. Con strict (módulo 1), TS te obliga a manejar ese null:

null-dom.ts
const boton = document.querySelector(".enviar");
// tipo: Element | null

boton.addEventListener("click", () => {});
// ❌ 'boton' is possibly 'null'.

// las tres formas de manejarlo, de más a menos segura:

// 1. chequeo explícito (la más clara):
if (boton) {
boton.addEventListener("click", () => {});   // 🔍 aquí boton no es null
}

// 2. optional chaining (para una acción puntual):
boton?.addEventListener("click", () => {});

// 3. non-null assertion '!' (PROMETES que existe — úsalo con cuidado):
boton!.addEventListener("click", () => {});
// solo si estás 100% seguro; si te equivocas, revienta en runtime como en JS

El null no es TS molestando: es TS mostrándote el caso que en JavaScript olvidabas. El chequeo if (boton) es lo ideal; el ?. sirve para una acción suelta; y el ! (non-null assertion) es una PROMESA tuya de que no es null —cómodo, pero si te equivocas, pierdes la red y vuelves al Cannot read properties of null. Úsalo solo cuando de verdad garantices la existencia (un elemento que tú mismo acabas de crear).

La verdad número dos: qué tipo de elemento

querySelector devuelve Element genérico —que NO tiene .value, .href, .src—. Para acceder a lo específico de un input, un enlace o una imagen, debes decir de qué tipo es:

tipo-elemento.ts
// Element genérico no tiene .value:
const input = document.querySelector(".email");
input.value;   // ❌ Property 'value' does not exist on type 'Element'.

// forma 1: el genérico de querySelector (la más limpia):
const email = document.querySelector<HTMLInputElement>(".email");
// tipo: HTMLInputElement | null
if (email) email.value = "hola@ejemplo.com";   // ✓ .value existe

// forma 2: aserción de tipo con 'as' (cuando no puedes usar el genérico):
const boton = document.querySelector(".btn") as HTMLButtonElement;
boton.disabled = true;   // ✓ .disabled existe en HTMLButtonElement

// cada elemento tiene su tipo: HTMLInputElement, HTMLAnchorElement,
// HTMLImageElement, HTMLFormElement, HTMLSelectElement...

querySelector<HTMLInputElement>(".email") le dice a TS "esto es un input", y a cambio te da .value, .checked, .placeholder con autocompletado. Prefiere el genérico <T> (más seguro, conserva el | null) sobre el as (que además AFIRMA que no es null). Cada tipo de elemento HTML tiene su interfaz: memorizar los nombres no hace falta —el autocompletado te los ofrece.

Tipar eventos y su target

Los eventos también tienen tipos precisos, y su target es genérico —hay que afirmarlo para leer, por ejemplo, el .value de un input:

eventos.ts
const form = document.querySelector<HTMLFormElement>("#form");

form?.addEventListener("submit", (evento: SubmitEvent) => {
evento.preventDefault();   // TS conoce los métodos de SubmitEvent
});

const input = document.querySelector<HTMLInputElement>("#nombre");

input?.addEventListener("input", (evento: Event) => {
// evento.target es EventTarget genérico (no tiene .value):
// const valor = evento.target.value;  // ❌

// se afirma el tipo del target:
const objetivo = evento.target as HTMLInputElement;
console.log(objetivo.value);   // ✓

// o, más seguro, usa 'currentTarget' o la variable 'input' del closure:
console.log(input.value);       // ✓ input ya está tipado como HTMLInputElement
});

Aquí un ejemplo corriendo (en JavaScript, pues el navegador no ejecuta tipos —pero la lógica es la que tipas arriba):

En TypeScript, ese document.querySelector("#nombre") lo tiparías como querySelector<HTMLInputElement>("#nombre") y manejarías el | null —y el navegador correría exactamente el mismo JavaScript, con los tipos ya borrados.

Escribes const input = document.querySelector('.campo') y luego input.value, y TS marca dos errores. ¿Cuáles son?

Mini-reto

Tipa una interacción (en tu editor con un proyecto TS): 1) selecciona un <input> con querySelector<HTMLInputElement> y maneja el | null; 2) selecciona un <button> y escúchale el click, tipando el evento como MouseEvent; 3) en el handler, lee el .value del input usando la variable tipada del closure (no evento.target). Confirma que TS te da .value sin quejas y te habría frenado si olvidabas el null.

Qué sigue

El DOM es una fuente de datos "de tu lado". La otra gran fuente vive en internet: las APIs. La próxima lección tipa fetch —y descubre su gran mentira: response.json() devuelve any, el agujero por el que se cuela todo lo que TypeScript intenta evitar. La solución te llevará a Zod.