TypeScript de Cero a Experto / TS en el mundo real
Tipar el DOM: querySelector, null y eventos
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:
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 JSEl 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:
// 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:
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):
<input id="nombre" placeholder="Escribe tu nombre" />
<p id="salida">Hola, invitado</p>body { font-family: system-ui; background: #0b1120; color: #f1f5f9; padding: 20px; }
input { padding: 8px; border-radius: 8px; border: 1px solid #2a3752; background: #151e31; color: #f1f5f9; }
p { color: #a78bfa; font-size: 18px; }const input = document.querySelector("#nombre");
const salida = document.querySelector("#salida");
input.addEventListener("input", () => {
salida.textContent = input.value ? "Hola, " + input.value : "Hola, invitado";
});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.