Narrowing y type guards
Narrowing es cómo TypeScript estrecha un tipo amplio a uno más específico dentro de un bloque. Los type guards son las herramientas para hacerlo — desde typeof hasta predicados personalizados.
¿Qué es el narrowing?
Cuando tienes una variable de tipo amplio (como string | number), TypeScript no sabe qué métodos están disponibles. El narrowing le permite deducir el tipo exacto en cada rama:
function procesar(val: string | number): string {
// Aquí val es string | number — no puedes llamar .toUpperCase() ni .toFixed()
if (typeof val === "string") {
// Aquí TypeScript sabe que val es string
return val.toUpperCase();
}
// Aquí TypeScript sabe que val es number (el único caso restante)
return val.toFixed(2);
}
console.log(procesar("hola")); // "HOLA"
console.log(procesar(3.14159)); // "3.14"HOLA
3.14typeof — narrowing de primitivos
function describir(val: unknown): string {
if (typeof val === "string") return `string: "${val}"`;
if (typeof val === "number") return `number: ${val}`;
if (typeof val === "boolean") return `boolean: ${val}`;
if (typeof val === "undefined") return "undefined";
if (typeof val === "object" && val !== null) return "objeto";
return "tipo desconocido";
}
// typeof null === "object" — el bug histórico de JS
// Por eso siempre verifica val !== null al narrowear object
console.log(describir("hola")); // string: "hola"
console.log(describir(42)); // number: 42
console.log(describir(null)); // objeto ← cuidado con nullstring: "hola"
number: 42instanceof — narrowing de clases
class Perro {
ladrar() { return "¡Guau!"; }
}
class Gato {
maullar() { return "¡Miau!"; }
}
function hacerRuido(animal: Perro | Gato): string {
if (animal instanceof Perro) {
return animal.ladrar(); // TypeScript sabe que es Perro
}
return animal.maullar(); // TypeScript sabe que es Gato
}
// instanceof también funciona con errores
try {
JSON.parse("invalid");
} catch (e) {
if (e instanceof SyntaxError) {
console.log("Error de JSON:", e.message);
}
}in — narrowing de propiedades
El operador in verifica si una propiedad existe en un objeto. Muy útil para discriminated unions:
interface Pájaro {
tipo: "pajaro";
volar(): string;
}
interface Pez {
tipo: "pez";
nadar(): string;
}
type Animal = Pájaro | Pez;
function mover(animal: Animal): string {
if ("volar" in animal) {
return animal.volar(); // TypeScript infiere: Pájaro
}
return animal.nadar(); // TypeScript infiere: Pez
}
// O con la propiedad discriminante
function mover2(animal: Animal): string {
if (animal.tipo === "pajaro") {
return animal.volar();
}
return animal.nadar();
}Type predicates — guards personalizados
Cuando la lógica de verificación es compleja, crea una función con un predicado de tipo (arg is Type):
interface Gato {
nombre: string;
maullar(): void;
}
interface Perro {
nombre: string;
ladrar(): void;
}
// Predicado: retorna boolean, pero TypeScript entiende la implicación de tipo
function esGato(animal: Gato | Perro): animal is Gato {
return "maullar" in animal;
}
function saludar(animal: Gato | Perro): void {
if (esGato(animal)) {
animal.maullar(); // ✅ TypeScript sabe que es Gato
} else {
animal.ladrar(); // ✅ TypeScript sabe que es Perro
}
}
// Ejemplo real: verificar que un valor no es null/undefined
function isDefined<T>(val: T | null | undefined): val is T {
return val !== null && val !== undefined;
}
const lista = [1, null, 2, undefined, 3];
const soloNumeros = lista.filter(isDefined); // number[]
console.log(soloNumeros); // [1, 2, 3][1, 2, 3]En TypeScript 3.7+ también puedes usar funciones de aserción (asserts x is Type). Si la función no lanza, TypeScript asume que el tipo es correcto para el resto del scope — útil para validaciones de entrada.