CodeForge

TypeScript de Cero a Experto / Narrowing

Exhaustividad con never: el guardián de los casos

Teoría18 min20 XP

Tu switch sobre una discriminated union maneja todos los casos hoy. ¿Pero qué pasa dentro de seis meses, cuando agregues una variante nueva y olvides su case? En JavaScript, un bug silencioso. En TypeScript tienes un guardián: el patrón de exhaustividad con never, que ROMPE la compilación hasta que manejes cada caso. Es el broche que hace tu red verdaderamente completa.

El inventario que no deja faltar nada

El truco: asignar a never

Recuerda del módulo 2: never es el tipo de lo imposible. Si un switch cubre TODOS los casos, en el default el valor es never (no queda ningún tipo posible). Asignarlo a una variable never compila… hasta que dejas un caso sin cubrir:

never-exhaustivo.ts
type Figura =
| { tipo: "circulo"; radio: number }
| { tipo: "cuadrado"; lado: number };

function area(figura: Figura): number {
switch (figura.tipo) {
  case "circulo": return Math.PI * figura.radio ** 2;
  case "cuadrado": return figura.lado ** 2;
  default:
    // si cubriste TODO, aquí 'figura' es never (imposible llegar). Compila.
    const _exhaustivo: never = figura;
    return _exhaustivo;
}
}

// AHORA agrega un caso a la unión y NO añadas su case:
//   | { tipo: "triangulo"; base: number; altura: number }
// → en el default, 'figura' ya NO es never (puede ser Triangulo),
//   y la línea 'const _exhaustivo: never = figura' DEJA DE COMPILAR:
//   ❌ Type 'Triangulo' is not assignable to type 'never'.
//   TS te obliga a agregar el case que olvidaste.

El mecanismo es hermoso por lo simple: mientras cubras todos los casos, el default es inalcanzable y figura es never, así que asignarla a _exhaustivo: never compila sin ruido. El día que agregas una variante y olvidas su case, ese nuevo tipo LLEGA al default, figura deja de ser never, y la asignación falla. El error te apunta exactamente al lugar y al caso que faltó.

El helper: assertNever

Para no repetir esas líneas en cada switch, se extrae un helper con nombre —el patrón estándar que verás en cualquier codebase TS seria:

assert-never.ts
// lanza en runtime Y sirve de guardián en compilación
function assertNever(valor: never): never {
throw new Error("Caso no manejado: " + JSON.stringify(valor));
}

type Estado = "cargando" | "exito" | "error";

function color(estado: Estado): string {
switch (estado) {
  case "cargando": return "gris";
  case "exito":    return "verde";
  case "error":    return "rojo";
  default:
    return assertNever(estado);   // 🛡️ guardián: rojo en compilación si falta un caso
}
}

Tienes un switch exhaustivo sobre una unión con assertNever en el default. Un compañero agrega una variante nueva a la unión pero olvida su case. ¿Qué pasa?

Mini-reto

Pon el guardián: 1) crea type Metodo = "GET" | "POST" | "DELETE" y una función permitido(m: Metodo): boolean con un switch que devuelva algo por cada método; 2) agrega assertNever en el default; 3) ahora añade "PUT" a la unión SIN su case y observa el error rojo en la línea de assertNever —luego arréglalo agregando el caso. Sentir cómo TS te obliga a completar es entender por qué este patrón da tanta tranquilidad.

Qué sigue

Cierras el módulo con un reto que junta discriminated unions, un switch exhaustivo y assertNever —modelando un sistema de eventos tipado de punta a punta. Después, el módulo 5 abre la puerta a la magia mayor de TypeScript: los GENÉRICOS, tipos que se adaptan para funcionar con cualquier dato sin perder la seguridad.