CodeForge

Maquetación Web Dinámica / TypeScript en la maquetación

Migrar los patrones del módulo 14 a TypeScript

Teoría20 min20 XP

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

modal.ts
// 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:

tabs.ts
// 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)

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.