Apariencia
📙 Clase 14 — Manipular el DOM con TypeScript (querySelector y tipos)
TypeScript · 2026-08-06 · Carpeta:
02-Ejercicios/DOM⬅️ Volver al índice de clases
🎯 Qué aprendí
- Cómo seleccionar elementos del HTML con
document.querySelectorpor etiqueta, clase (.title) o ID (#message), y por qué en los tres casos el tipo que regresa TypeScript es genérico (Element | null), no el tipo real del elemento. - Por qué hace falta una type assertion (
as HTMLInputElement, etc.) para acceder a propiedades específicas de cada tipo de elemento (.value,.placeholder,.disabled…) — sin ella, TypeScript ni siquiera deja compilar. - Por qué el operador de encadenamiento opcional (
?.) resuelve el problema del posiblenull, pero no resuelve el problema del tipo específico — son dos problemas distintos con dos soluciones distintas. - Cómo usar
tsc archivo.ts --watch(o-w) para que el compilador recompile solo cada vez que guardo, sin tener que corrertsca mano una y otra vez. - Que se puede declarar una variable con un tipo amplio (
HTMLElement) y asignarle después un valor de un tipo más específico (HTMLButtonElement) usandoas. - Un detalle que corregí en mi propio código:
HTMLHeadElementno es lo mismo queHTMLHeadingElement(ver sección 3 de la teoría).
📚 Definiciones clave
| Término | Qué es | Ejemplo de esta clase |
|---|---|---|
document.querySelector(selector) | Busca el primer elemento que cumple el selector CSS; devuelve Element | null | document.querySelector('#message') |
as HTMLInputElement | Type assertion: le dice a TypeScript el tipo real del elemento, para poder usar sus propiedades específicas | ... as HTMLInputElement |
?. (optional chaining) | Accede a una propiedad solo si el valor no es null/undefined; si lo es, da undefined en vez de tronar | elemento?.textContent |
tsc archivo.ts --watch / -w | Recompila automáticamente cada vez que guardas el .ts | tsc main.ts -w |
📖 PARTE TEÓRICA
🔍 1. querySelector: tres formas de seleccionar un elemento
document.querySelector acepta cualquier selector CSS — por etiqueta, clase (con .) o ID (con #):
typescript
// Por nombre de etiqueta
const h1 = document.querySelector('h1');
// Por clase
const titulo = document.querySelector('.title') as HTMLHeadingElement;
// Por ID
const message = document.querySelector('#message') as HTMLInputElement;En los tres casos, sin importar el selector, querySelector en TypeScript devuelve Element | null — el tipo más genérico posible. TypeScript no tiene forma de saber, solo mirando el string '#message', que ese ID corresponde a un <input> en tu HTML; esa relación solo existe en tu cabeza y en el archivo .html, no en el tipo.
🧪 Entrevista: ¿Qué tipo devuelve
document.querySelector()? SiempreElement | null, sin importar el selector que uses — TypeScript no analiza tu HTML para inferir el tipo real del elemento.
🎭 2. Type assertion: decirle a TypeScript qué tipo esperas
Como querySelector no puede inferir el tipo específico, se usa as (type assertion) para indicárselo tú mismo:
typescript
const message = document.querySelector('#message') as HTMLInputElement;
console.log(message.placeholder); // ahora sí: placeholder existe en HTMLInputElementTypeScript trae interfaces específicas para casi cualquier elemento HTML: HTMLInputElement (inputs), HTMLButtonElement (botones), HTMLHeadingElement (h1–h6), y muchas más — cada una con las propiedades reales de ese elemento (.value, .disabled, .placeholder…).
Verificado en terminal: sin la asignación, acceder a una propiedad específica es un error de compilación, no de ejecución — nunca llegas a correr el código roto:
$ tsc sin-asercion.ts
error TS18047: 'message' is possibly 'null'.
error TS2339: Property 'placeholder' does not exist on type 'Element'.📝 3. Un errata corregida: HTMLHeadElement ≠ HTMLHeadingElement
Al escribir el ejercicio me equivoqué con un nombre parecido, y vale la pena dejarlo documentado porque es un error fácil de repetir:
| Tipo | Es para | ¿Sirve para <h1 class="title">? |
|---|---|---|
HTMLHeadElement | La etiqueta <head> del documento (metadatos, no contenido visible) | ❌ No |
HTMLHeadingElement | Los encabezados <h1> a <h6> | ✅ Sí |
typescript
// ❌ compila, pero el tipo es semánticamente incorrecto
let titulo: HTMLHeadElement = document.querySelector('.title') as HTMLHeadElement;
// ✅ el tipo correcto para un <h1>
let titulo: HTMLHeadingElement = document.querySelector('.title') as HTMLHeadingElement;⚠️ TypeScript no marca error en la versión incorrecta —
HTMLHeadElementyHTMLHeadingElementson primos lejanos (ambos heredan deHTMLElement), así que la aserciónaspasa sin quejarse. El problema es semántico, no de compilación: le estás mintiendo al lector de tu código sobre qué representa esa variable. Vale la pena revisar el nombre exacto del tipo con autocompletado de VS Code antes de escribirlo.
❔ 4. ?. resuelve el null, pero no el tipo
Estos son dos problemas distintos que suelen aparecer juntos, y confundirlos genera errores raros. Verificado en terminal, paso a paso:
typescript
const message = document.querySelector('#message');
console.log(message.placeholder);→ dos errores: TS18047 (posible null) y TS2339 (la propiedad no existe en Element).
typescript
const message = document.querySelector('#message');
console.log(message?.placeholder);→ el ?. resuelve el null, pero sigue quedando TS2339 — Element sigue sin tener .placeholder, sin importar si accedes con . o con ?..
typescript
const message = document.querySelector('#message') as HTMLInputElement;
console.log(message?.placeholder);→ compila limpio. La as resuelve el tipo; el ?. resuelve el null. Necesitas ambos cuando el elemento podría no existir Y quieres una propiedad específica.
💡 Piensa en
?.como "¿existe esto?" y enas Tipocomo "¿qué es esto?" — son preguntas distintas, yquerySelectordeja las dos abiertas a la vez.
🔁 5. Modo --watch: recompilar sin correr tsc a mano
Escribir código, compilar, revisar el navegador, repetir — hacerlo a mano con tsc archivo.ts cada vez se vuelve tedioso. El modo watch automatiza el paso de compilar:
bash
tsc main.ts --watch
# o, abreviado:
tsc main.ts -wVerificado en terminal: dejé el watcher corriendo, edité el .ts sin tocar la terminal para nada, y el .js se actualizó solo:
$ tsc main.ts --watch
Starting compilation in watch mode...
Found 0 errors. Watching for file changes.
← (edito y guardo el .ts en otra ventana, sin tocar esta terminal)
File change detected. Starting incremental compilation...
Found 0 errors. Watching for file changes.💡 El watcher se queda corriendo (no regresa el control de la terminal) hasta que lo detienes con
Ctrl+C. El flujo cómodo: una terminal integrada de VS Code contsc -wcorriendo, tu editor al lado, y el navegador en otra pestaña con recarga manual o Live Server — igual que viste en la Clase 12.
🔼 6. Dato curioso: de un tipo amplio a uno específico
Se puede declarar una variable con el tipo genérico HTMLElement y, más adelante, asignarle un valor de un tipo más concreto usando as:
typescript
let elemento: HTMLElement;
elemento = document.querySelector('button') as HTMLButtonElement; // de HTMLElement a HTMLButtonElementFunciona porque HTMLButtonElement extiende HTMLElement — es válido asignar algo más específico a una variable declarada como algo más general (todo botón ES un HTMLElement, aunque no todo HTMLElement sea un botón). Es la misma idea de jerarquía de tipos que ya viste con las interfaces en la Clase 5.
💻 PARTE PRÁCTICA
Archivos trabajados: 02-Ejercicios/DOM/index.html y 02-Ejercicios/DOM/main.ts
typescript
// main.ts
const h1 = document.querySelector('h1');
console.log(h1?.textContent);
let title: HTMLHeadingElement = document.querySelector('.title') as HTMLHeadingElement;
console.log(title?.textContent);
const message = document.querySelector('#message') as HTMLInputElement;
console.log(message?.placeholder);
console.log('Hola de nuevo');bash
cd 02-Ejercicios/DOM
tsc main.tsSalida real verificada — servido en localhost real y confirmada en la consola de Chrome:
Mi curso de Typescript en Platzi
Mi curso de Typescript en Platzi
Mensaje del usuario
Hola de nuevoTodo verificado y corriendo. ✅
🏋️ EJERCICIOS CON SOLUCIÓN
Ejercicio 1 — Seleccionar por etiqueta y mostrar el texto
Selecciona el <h1> del HTML con querySelector('h1') y muestra su .textContent en consola, usando ?. para evitar el error de null.
💡 ¿Sabías que…? — `textContent` incluye TODO el texto, hasta el de hijos anidados
.textContent junta el texto de un elemento y de todos sus elementos hijos, sin importar cuántos niveles de anidación tengan — a diferencia de mirar solo el texto directo. Ejemplo de referencia:
typescript
const parrafo = document.querySelector('p');
console.log(parrafo?.textContent); // el texto completo, aunque tenga <span> adentroVer solución
typescript
const h1 = document.querySelector('h1');
console.log(h1?.textContent);Consola esperada (con el HTML de la práctica):
Mi curso de Typescript en PlatziEjercicio 2 — Seleccionar por clase con el tipo correcto
Selecciona .title con querySelector y asígnalo a una variable tipada HTMLHeadingElement (el tipo correcto para un <h1>, no HTMLHeadElement).
💡 ¿Sabías que…? — cada etiqueta HTML tiene su propia interfaz en TypeScript
TypeScript trae una interfaz distinta para casi cada etiqueta HTML — HTMLParagraphElement para <p>, HTMLAnchorElement para <a>, HTMLHeadingElement para <h1>–<h6>. Todas extienden HTMLElement, que a su vez extiende Element. Ejemplo de referencia:
typescript
const enlace = document.querySelector('a') as HTMLAnchorElement;
console.log(enlace?.href); // .href solo existe en HTMLAnchorElementVer solución
typescript
let titulo: HTMLHeadingElement = document.querySelector('.title') as HTMLHeadingElement;
console.log(titulo?.textContent);Terminal esperada:
Mi curso de Typescript en PlatziEjercicio 3 — Seleccionar por ID y leer una propiedad específica
Selecciona #message con el tipo HTMLInputElement y muestra su .placeholder en consola.
💡 ¿Sabías que…? — `HTMLInputElement` tiene propiedades que `Element` no tiene
.value, .placeholder, .disabled, .checked… son propiedades que solo existen en HTMLInputElement, no en el tipo genérico Element. Por eso la aserción as es obligatoria aquí — sin ella, TypeScript ni te deja escribir .placeholder. Ejemplo de referencia:
typescript
const casilla = document.querySelector('#acepto') as HTMLInputElement;
console.log(casilla?.checked);Ver solución
typescript
const message = document.querySelector('#message') as HTMLInputElement;
console.log(message?.placeholder);Terminal esperada:
Mensaje del usuarioEjercicio 4 — Forzar los dos errores de saltarte ?. y as
Antes de correrlo: si seleccionas #message SIN as HTMLInputElement y SIN ?., y accedes a .placeholder, ¿cuántos errores esperas? Anticípalo y compruébalo.
💡 ¿Sabías que…? — `querySelector` deja DOS problemas abiertos a la vez
Element | null son dos riesgos distintos en un solo tipo: "podría no existir" (null) y "no sé qué tan específico es" (Element genérico). Resolver solo uno dejará el otro error en pie. Ejemplo de referencia: la sección 4 de la teoría muestra los tres casos (sin nada, solo ?., as + ?.) verificados en terminal.
Ver solución
typescript
const message = document.querySelector('#message');
console.log(message.placeholder);Terminal real — dos errores, uno por cada problema sin resolver:
$ tsc sin-asercion.ts
error TS18047: 'message' is possibly 'null'.
error TS2339: Property 'placeholder' does not exist on type 'Element'.💡 Agregar solo
?.quita el primer error (TS18047) pero deja el segundo (TS2339) — hace falta también elas HTMLInputElementpara que desaparezcan los dos.
Ejercicio 5 — Compilar en modo --watch
Deja tsc main.ts --watch corriendo en una terminal. Sin detenerlo, cambia el mensaje de un console.log en main.ts, guarda, y confirma (sin volver a correr tsc) que el .js se actualizó solo.
💡 ¿Sabías que…? — el watcher no libera la terminal hasta `Ctrl+C`
A diferencia de tsc main.ts normal (que compila y termina), --watch deja el proceso corriendo indefinidamente, "escuchando" cambios en el archivo — tu terminal queda ocupada por ese proceso hasta que lo detengas manualmente. Por eso en VS Code conviene usarlo en una terminal integrada dedicada, dejando otra libre para otros comandos.
Ver solución
bash
tsc main.ts --watchSin tocar esa terminal, edito y guardo main.ts en el editor. Terminal real, tras guardar:
Starting compilation in watch mode...
Found 0 errors. Watching for file changes.
File change detected. Starting incremental compilation...
Found 0 errors. Watching for file changes.💡 La forma abreviada
-whace exactamente lo mismo:tsc main.ts -w.
Ejercicio 6 — De tipo amplio a tipo específico
Declara let boton: HTMLElement; y, en una línea aparte, asígnale document.querySelector('button') as HTMLButtonElement. Confirma que compila.
💡 ¿Sabías que…? — todo `HTMLButtonElement` ES un `HTMLElement`, no al revés
Los tipos de elementos HTML forman una jerarquía (Element → HTMLElement → HTMLButtonElement, etc.). Puedes guardar algo específico en una variable declarada como algo más general — nunca al revés sin una aserción. Ejemplo de referencia:
typescript
let campo: HTMLElement;
campo = document.querySelector('#nombre') as HTMLInputElement; // válidoVer solución
typescript
let elemento: HTMLElement;
elemento = document.querySelector('button') as HTMLButtonElement;
console.log(elemento);Verificado: compila sin errores (tsc tipo-amplio.ts → 0 errores).
Ejercicio 7 — Cambiar una propiedad, no solo leerla
Usa #message como HTMLInputElement y, en vez de solo leer .placeholder, cámbialo a un texto nuevo con una asignación (message.placeholder = "...").
💡 ¿Sabías que…? — las propiedades del DOM son de doble vía
A diferencia de .textContent en un <h1> (que puedes leer y también reasignar para cambiar el texto visible), las propiedades tipadas como .placeholder funcionan igual: leerlas te da el valor actual, y asignarles un valor nuevo actualiza el elemento real en la página al instante. Ejemplo de referencia:
typescript
const boton = document.querySelector('button') as HTMLButtonElement;
boton.disabled = true; // deshabilita el botón de verdad, en la páginaVer solución
typescript
const message = document.querySelector('#message') as HTMLInputElement;
message.placeholder = "Escribe tu nombre aquí";
console.log(message.placeholder);Terminal esperada:
Escribe tu nombre aquí💡 Si recargas la página verás el nuevo placeholder en el
<input>real — no es solo una copia en memoria, es el elemento vivo del DOM.
Ejercicio 8 — HTMLHeadElement vs HTMLHeadingElement: repetir el error a propósito
Declara let titulo: HTMLHeadElement = document.querySelector('h1') as HTMLHeadElement; (el tipo INCORRECTO, a propósito) y confirma que TypeScript no lo marca como error de compilación, aunque semánticamente esté mal.
💡 ¿Sabías que…? — `as` solo valida que los tipos estén "relacionados", no que sean el correcto
Una type assertion (as) le pide a TypeScript que confíe en ti — solo la rechaza si los dos tipos son completamente ajenos entre sí (por ejemplo, as string sobre un number sin pasar por unknown). Como HTMLHeadElement y HTMLHeadingElement comparten un ancestro común (HTMLElement), TypeScript los considera "relacionados" y deja pasar la aserción, aunque sea el tipo incorrecto para ese elemento.
Ver solución
typescript
// compila, pero es SEMÁNTICAMENTE incorrecto (HTMLHeadElement es para <head>, no <h1>)
let titulo: HTMLHeadElement = document.querySelector('h1') as HTMLHeadElement;
console.log(titulo.textContent); // funciona en tiempo de ejecución, mala señal en el códigoVerificado: tsc compila sin ningún error — el problema es invisible para el compilador, solo lo detecta un lector humano (o tú mismo, revisando el nombre del tipo con cuidado).
💡 La forma de evitarlo: dejar que el autocompletado de VS Code te muestre las opciones al escribir
HTMLHeady elegir la que dice "Heading", no "Head" a secas.
Ejercicio 9 — Script al final del <body> en vez de defer
Quita el atributo defer del <script> en el <head> y, en su lugar, mueve la etiqueta <script src="main.js"></script> al final del <body>, después de todos los elementos. Confirma que querySelector sigue funcionando igual.
💡 ¿Sabías que…? — `defer` y "al final del body" resuelven el MISMO problema
Ambas técnicas garantizan que el HTML ya esté completo cuando tu script corre — solo cambia el mecanismo. defer deja el script en el <head> pero le dice al navegador "ejecútame después de parsear todo el HTML". Poner el <script> al final del <body> logra lo mismo por orden natural: para cuando el navegador llega a esa línea, ya leyó todo lo de arriba. Ya viste el problema que resuelven en la Clase 12: sin ninguna de las dos, querySelector puede devolver null porque el elemento todavía no existe.
Ver solución
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<h1 class="title">Mi curso de Typescript en Platzi</h1>
<label for="message">Escribe algo:</label>
<input type="text" id="message" placeholder="Mensaje del usuario" />
<script src="main.js"></script>
</body>
</html>Terminal/consola esperada — igual que con defer, porque ambas garantizan que el DOM ya existe:
Mi curso de Typescript en PlatziEjercicio 10 — Integrador: leer, validar y mostrar con --watch de fondo
Con tsc main.ts --watch corriendo, escribe un script que lea #message, y si su .value está vacío muestre un mensaje de aviso en consola; si no, muestre `Escribiste: ${valor}`. Prueba cambiando el console.log mientras el watcher sigue corriendo, sin recompilar a mano.
💡 ¿Sabías que…? — integrar `--watch` con lógica de DOM es el flujo real de trabajo
Este ejercicio no trae ningún concepto nuevo — junta todo lo de la clase: as HTMLInputElement para el tipo, ?. o una validación explícita para el posible null, y --watch para no tener que recompilar a mano en cada prueba. Es, literalmente, el flujo con el que se trabaja el resto del curso de aquí en adelante.
Ver solución
typescript
const message = document.querySelector('#message') as HTMLInputElement;
if (message?.value.trim() === "") {
console.log("⚠️ El campo está vacío");
} else {
console.log(`Escribiste: ${message?.value}`);
}bash
tsc main.ts --watchTerminal esperada (watcher de fondo, sin volver a correr tsc a mano):
Found 0 errors. Watching for file changes.
File change detected. Starting incremental compilation...
Found 0 errors. Watching for file changes.❓ Preguntas y respuestas (autoevaluación)
1. ¿Qué tipo devuelve document.querySelector(selector), sin importar el selector?
Siempre
Element | null— TypeScript no analiza tu HTML para saber qué tipo específico de elemento corresponde a ese selector.
2. ¿Para qué sirve as HTMLInputElement (o similar) después de un querySelector?
Es una type assertion: le dice a TypeScript el tipo real del elemento, para poder usar sus propiedades específicas (
.value,.placeholder,.disabled…) sin error de compilación.
3. ¿Qué problema resuelve ?. (optional chaining) al usar querySelector?
El de que el elemento podría no existir (
querySelectordevuelvenullsi no encuentra nada) —?.evita el error accediendo de forma segura, devolviendoundefineden vez de tronar.
4. Si uso ?. pero NO uso as Tipo, ¿desaparecen todos los errores de compilación?
No.
?.solo resuelve el problema del posiblenull(TS18047). Si intentas usar una propiedad específica (como.placeholder) sobre el tipo genéricoElement, sigue quedando el errorTS2339: Property does not exist.
5. ¿Cuál es la diferencia entre HTMLHeadElement y HTMLHeadingElement?
HTMLHeadElementes el tipo de la etiqueta<head>del documento.HTMLHeadingElementes el tipo correcto para<h1>–<h6>. Son nombres parecidos pero representan cosas distintas — TypeScript no te avisa si los confundes, porque ambos derivan deHTMLElement.
6. ¿Qué hace tsc archivo.ts --watch (o -w) que no hace tsc archivo.ts normal?
Deja el compilador corriendo de forma continua, recompilando automáticamente cada vez que detecta un cambio guardado en el archivo — sin que tengas que volver a correr
tsca mano.
7. ¿Puedo asignar un valor de tipo HTMLButtonElement a una variable declarada como HTMLElement?
Sí.
HTMLButtonElementextiendeHTMLElement, así que es válido guardar algo más específico en una variable declarada como algo más general (la relación inversa sí necesitaría una aserción).
8. ¿Qué significa el error TS18047?
Que una variable "posiblemente es
null" (oundefined) — típicamente aparece conquerySelectorcuando accedes a una propiedad sin?.ni una comprobación previa.
9. ¿defer en el <script> y poner el <script> al final del <body> resuelven el mismo problema?
Sí — ambos garantizan que el HTML ya está completamente cargado antes de que tu script intente hacer
querySelector. Sin ninguno de los dos, el script podría correr antes de que el elemento exista, yquerySelectordevolveríanull.
10. ¿Por qué document.querySelector('#message').placeholder (sin ?. ni as) da DOS errores en vez de uno?
Porque son dos problemas distintos en el mismo tipo
Element | null:TS18047(posiblenull, sin resolver) yTS2339(.placeholderno existe en el tipo genéricoElement, sin resolver). Cada uno necesita su propia solución (?.yas Tipo).
📎 Apuntes relacionados
- Clase 5: interfaces y jerarquía de tipos
- Clase 12:
defer, DOM tipado congetElementById - Conceptos
- Comandos
➡️ Siguiente
Utility types — Partial, Pick, Omit, Record… transformar tipos existentes sin reescribirlos.