TypeScript de Cero a Experto / Narrowing
Exhaustividad con never: el guardián de los casos
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:
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:
// 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.