Maquetación Web Dinámica / TypeScript en la maquetación
Migrar los patrones del módulo 14 a TypeScript
Ya tienes las tres piezas —elementos tipados, null manejado, eventos
tipados—; ahora las juntas en código real. Tomamos los patrones de UI que
construiste en el módulo 14 (el modal, las tabs, el dark mode) y los
reescribimos en TypeScript. Mismo comportamiento, misma línea de código en
esencia... pero blindados contra los errores tontos que en JavaScript solo
descubrirías en producción.
El modal, ahora tipado
// selección tipada: cada elemento con su tipo específico
const abrir = document.querySelector<HTMLButtonElement>("#abrir");
const modal = document.querySelector<HTMLElement>("#modal");
const cerrar = document.querySelector<HTMLButtonElement>("#cerrar");
let previo: HTMLElement | null = null; // variable tipada explícita
// UN null-guard protege todo el bloque: dentro, TS sabe que los tres existen
if (abrir && modal && cerrar) {
abrir.addEventListener("click", () => {
previo = document.activeElement as HTMLElement; // el foco actual
modal.classList.add("abierto");
modal.setAttribute("aria-hidden", "false");
cerrar.focus();
});
const cerrarModal = () => {
modal.classList.remove("abierto");
modal.setAttribute("aria-hidden", "true");
previo?.focus(); // optional chaining: si hay previo, enfócalo
};
cerrar.addEventListener("click", cerrarModal);
document.addEventListener("keydown", (event) => { // event: KeyboardEvent
if (event.key === "Escape" && modal.classList.contains("abierto")) cerrarModal();
});
}La lógica es idéntica a la del módulo 14; lo que cambió es la seguridad: el
if (abrir && modal && cerrar) de arriba hace que, dentro, TypeScript sepa
que ninguno es null (nada de comprobar uno por uno), previo está tipado
para que previo?.focus() sea seguro, y event.key viene con
autocompletado. Un typo como modal.classList.ad(...) se subrayaría al
instante.
Varios elementos: querySelectorAll tipado
Para las tabs (muchos botones), tipas la colección entera:
// el genérico también funciona en querySelectorAll
const tabs = document.querySelectorAll<HTMLButtonElement>(".tab");
tabs.forEach((tab) => {
tab.addEventListener("click", () => { // 'tab' ya es HTMLButtonElement
tabs.forEach((t) => t.setAttribute("aria-selected", "false"));
tab.setAttribute("aria-selected", "true");
// tab.dataset.panel está tipado como string | undefined → manéjalo
});
});
// dark mode: localStorage devuelve string | null (TS te lo recuerda)
const guardado: string | null = localStorage.getItem("tema");
if (guardado === "oscuro") document.body.classList.add("oscuro");Nota tab.dataset.panel: TypeScript sabe que un dataset puede ser
undefined, así que te "obliga" a considerar el caso —justo el tipo de
detalle que en JS olvidas hasta que rompe—. Y localStorage.getItem
devuelve string | null: TS te lo tipa para que no asumas que siempre hay
valor.
Corriendo (el modal tipado, ya compilado)
<button id="abrir" class="btn">Abrir modal</button>
<div id="modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="t" aria-hidden="true">
<div class="dialogo">
<h2 id="t">Modal en TypeScript</h2>
<p>Mismo comportamiento del módulo 14, ahora blindado por tipos. Cierra con el botón o Escape.</p>
<button id="cerrar" class="btn">Cerrar</button>
</div>
</div>body { font-family: system-ui; padding: 20px; background: #f8fafc; }
.btn { padding: 10px 18px; border: 0; border-radius: 8px; background: #6d28d9; color: white; font-weight: 600; cursor: pointer; }
.btn:focus-visible { outline: 3px solid #a78bfa; outline-offset: 2px; }
.modal { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(15,23,42,0.6); opacity: 0; visibility: hidden; transition: opacity 200ms ease; padding: 16px; }
.modal.abierto { opacity: 1; visibility: visible; }
.dialogo { background: white; border-radius: 14px; padding: 22px; max-width: 320px; display: grid; gap: 10px; }
.dialogo h2 { margin: 0; font-size: 18px; color: #1e293b; }
.dialogo p { margin: 0; font-size: 14px; color: #475569; }
@media (prefers-reduced-motion: reduce) { .modal { transition: none; } }const abrir = document.querySelector("#abrir");
const modal = document.querySelector("#modal");
const cerrar = document.querySelector("#cerrar");
let previo = null;
if (abrir && modal && cerrar) {
abrir.addEventListener("click", () => {
previo = document.activeElement;
modal.classList.add("abierto");
modal.setAttribute("aria-hidden", "false");
cerrar.focus();
});
const cerrarModal = () => {
modal.classList.remove("abierto");
modal.setAttribute("aria-hidden", "true");
if (previo) previo.focus();
};
cerrar.addEventListener("click", cerrarModal);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modal.classList.contains("abierto")) cerrarModal();
});
}Lo ves funcionar igual que en el módulo 14 —porque ES el mismo JS
compilado—. La diferencia vive en tu editor: al ESCRIBIRLO en TypeScript,
cada null, cada typo, cada evento mal usado se habría subrayado antes
de llegar aquí.
Migras tu modal a TS y envuelves todo en if (abrir && modal && cerrar). ¿Qué ventaja concreta te da ese guard además de evitar el crash por null?
Mini-reto
Toma UNO de tus patrones del módulo 14 (el acordeón o las tabs) y
reescríbelo en un Vite vanilla-ts: selecciona con tipos, envuelve en un
null-guard, tipa el evento. Rómpelo a propósito: escribe mal una propiedad
(classList.tggle) y mira el subrayado rojo; quita el guard y mira el aviso
de null. Cada error que TS te muestra es un bug que NO llegará a tus
usuarios.
Qué sigue
Tipaste elementos y comportamiento. Falta lo más poderoso: tipar tus DATOS. La próxima lección crea interfaces para tus objetos (una noticia, un producto) y renderiza desde ellos con seguridad total — de modo que si un dato cambia de forma, TypeScript te avisa en cada lugar que lo usa.