El narrowing es uno de los mecanismos más potentes de TypeScript: el compilador analiza el flujo de ejecución y reduce automáticamente el tipo de una variable en cada rama. No necesitas aserciones manuales ni castings; TypeScript lo hace solo cuando le das las pistas correctas. Esta guía cubre los guards más habituales y el patrón de discriminated unions.
typeof: distinguir primitivos
typeof es el guard más simple. TypeScript lo entiende y en cada rama conoce el tipo exacto de la variable:
function procesar(valor: string | number | boolean): string {
if (typeof valor === "string") {
return valor.toUpperCase(); // valor: string
}
if (typeof valor === "number") {
return valor.toFixed(2); // valor: number
}
return valor ? "sí" : "no"; // valor: boolean
}
typeof funciona bien con primitivos pero tiene un problema conocido: typeof null === "object". Para null, usa una comparación de igualdad estricta (valor === null).
instanceof: comprobar clases
instanceof comprueba si un valor es instancia de una clase concreta. TypeScript lo usa para narrowing cuando la variable puede ser de varios tipos de clase:
class ErrorRed extends Error {
constructor(public codigo: number, mensaje: string) {
super(mensaje);
}
}
class ErrorValidacion extends Error {
constructor(public campo: string, mensaje: string) {
super(mensaje);
}
}
function manejarError(e: ErrorRed | ErrorValidacion): void {
if (e instanceof ErrorRed) {
console.error(`Error de red ${e.codigo}: ${e.message}`);
} else {
console.error(`Campo inválido ${e.campo}: ${e.message}`);
}
}
El operador in: comprobar propiedades
in comprueba si una propiedad existe en un objeto. Es útil para discriminar tipos que no tienen una propiedad discriminante explícita con literales:
interface Perro { nombre: string; ladrar(): void }
interface Gato { nombre: string; maullar(): void }
function hablar(animal: Perro | Gato): void {
if ("ladrar" in animal) {
animal.ladrar(); // animal: Perro
} else {
animal.maullar(); // animal: Gato
}
}
Type guards personalizados con is
Cuando ninguno de los guards anteriores sirve, puedes escribir tu propia función de comprobación. El tipo de retorno param is Tipo le dice a TypeScript que si la función devuelve true, el parámetro es de ese tipo:
interface Articulo {
id: number;
titulo: string;
url: string;
}
function esArticulo(valor: unknown): valor is Articulo {
return (
typeof valor === "object" &&
valor !== null &&
"id" in valor &&
"titulo" in valor &&
"url" in valor
);
}
const respuesta: unknown = await fetch("/api/articulo/1").then(r => r.json());
if (esArticulo(respuesta)) {
console.log(respuesta.titulo); // respuesta: Articulo
}
Discriminated unions: el narrowing más robusto
El patrón más limpio para narrowing complejo es el discriminated union: cada variante del tipo tiene una propiedad literal única. TypeScript lo resuelve con un switch sin necesidad de guards adicionales:
type Evento =
| { tipo: "click"; x: number; y: number }
| { tipo: "tecla"; codigo: string; ctrl: boolean }
| { tipo: "scroll"; delta: number };
function procesarEvento(ev: Evento): void {
switch (ev.tipo) {
case "click":
console.log(`Click en (${ev.x}, ${ev.y})`);
break;
case "tecla":
console.log(`Tecla ${ev.codigo}${ev.ctrl ? " + Ctrl" : ""}`);
break;
case "scroll":
console.log(`Scroll ${ev.delta}px`);
break;
}
}
En cada case, TypeScript sabe exactamente qué propiedades existen. No hay riesgo de acceder a ev.x en el case de "tecla": el compilador lo detectaría como error.
El narrowing bien aplicado elimina la necesidad de castings y type assertions en la mayor parte del código de aplicación. Cuando te veas escribiendo as NombreTipo, plantéate si un type guard o un discriminated union puede resolver el problema sin la aserción manual.
Imagen: Pexels / Pixabay
