CodeForge

TypeScript de Cero a Experto / Narrowing

Discriminated unions: el patrón más útil de TS

Teoría22 min20 XP

Si te quedas con UN patrón de todo TypeScript, que sea este. Las discriminated unions modelan "una de varias formas posibles" con una elegancia que hace el código a prueba de errores: cada variante lleva una etiqueta, y TS estrecha por ella con precisión total. Modelan estados, acciones, respuestas de API —está en el corazón de Redux, de los reducers de React, de todo TS profesional.

La etiqueta que lo dice todo

El ingrediente: un campo discriminante

Una discriminated union es una unión de objetos que COMPARTEN un campo con un tipo LITERAL distinto en cada uno —ese campo es el "discriminante":

discriminada.ts
// cada variante tiene 'tipo' con un literal ÚNICO: esa es la etiqueta
type Circulo = { tipo: "circulo"; radio: number };
type Rectangulo = { tipo: "rectangulo"; ancho: number; alto: number };
type Triangulo = { tipo: "triangulo"; base: number; altura: number };

type Figura = Circulo | Rectangulo | Triangulo;

function area(figura: Figura): number {
// el switch sobre el discriminante estrecha cada caso con precisión:
switch (figura.tipo) {
  case "circulo":
    return Math.PI * figura.radio ** 2;        // 🔍 aquí figura es Circulo
  case "rectangulo":
    return figura.ancho * figura.alto;         // 🔍 Rectangulo
  case "triangulo":
    return (figura.base * figura.altura) / 2;  // 🔍 Triangulo
}
}

console.log(area({ tipo: "circulo", radio: 5 }).toFixed(2));
console.log(area({ tipo: "rectangulo", ancho: 4, alto: 3 }));

Dentro de cada case, TS SABE la variante exacta: en case "circulo", figura es un Circulo y te ofrece radio (pero NO ancho, que no tiene). El campo tipo es la llave que abre cada caso. Comparado con el "radio" in figura de la lección 2, esto es más robusto: no dependes de qué campo es único, sino de una etiqueta explícita y diseñada para distinguir.

El caso que verás mil veces: estados de carga

El uso estrella. Toda app que pide datos tiene tres estados, y modelarlos como discriminated union hace IMPOSIBLES las combinaciones sin sentido:

estados.ts
type Estado =
| { estado: "cargando" }
| { estado: "exito"; datos: string[] }
| { estado: "error"; mensaje: string };

function pintar(s: Estado): string {
switch (s.estado) {
  case "cargando": return "⏳ Cargando...";
  case "exito":    return "✓ " + s.datos.length + " resultados";  // datos existe AQUÍ
  case "error":    return "✗ " + s.mensaje;                       // mensaje existe AQUÍ
}
}

// lo poderoso: es IMPOSIBLE tener 'datos' sin estar en éxito, o 'mensaje'
// sin estar en error. El tipo prohíbe los estados imposibles por diseño.

Fíjate en la garantía: datos SOLO existe cuando estado es "exito", y mensaje SOLO cuando es "error". En JavaScript tendrías un objeto { cargando, datos, error } con todos los campos opcionales, y nada te impediría el absurdo "cargando Y con error Y con datos a la vez". La discriminated union hace ese absurdo IMPOSIBLE de escribir —el estado imposible ni siquiera compila.

Ese reducer es, literalmente, cómo funciona el manejo de estado en React con useReducer y en Redux: las acciones son una discriminated union por su campo tipo, y el switch estrecha cada una para acceder a sus datos con seguridad. Aprender este patrón ahora es entender de raíz una de las arquitecturas más usadas del frontend.

¿Qué hace a una unión ser 'discriminada' y por qué es mejor que estrechar con 'in'?

Mini-reto

Modela con etiquetas: 1) crea type Evento = { tipo: "click"; x: number; y: number } | { tipo: "tecla"; codigo: string } | { tipo: "scroll"; delta: number }; 2) escribe describir(e: Evento): string con un switch sobre e.tipo que use los campos propios de cada caso; 3) confirma que dentro de case "click" TS te ofrece x/y pero NO codigo. Acabas de modelar eventos como los modela cualquier sistema serio: imposibles de confundir.

Qué sigue

Tu switch sobre el discriminante cubre todos los casos… ¿pero qué pasa el día que agregues una variante nueva y olvides su case? La última lección del módulo te da un guardián automático para eso: la exhaustividad con never, que rompe la compilación hasta que manejes TODOS los casos. Tu red, completa.