TypeScript es muy listo, pero a veces necesita “pistas” para saber exactamente de qué tipo es una variable en un momento dado. Este proceso de refinar un tipo amplio (como string | number) a uno más específico (como string) se llama Narrowing.
Y para lograrlo, usamos Type Guards (Guardias de Tipos).
1. El problema: La incertidumbre
Imagina que tienes una función que acepta un ID, pero este ID puede ser un número (Base de datos SQL) o un texto (UUID).
function printId(id: string | number) {
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
console.log(id.toUpperCase());
}
printId("abc");function printId(id: string | number) {
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
console.log(id.toUpperCase());
}
printId("abc");TypeScript se queja porque no está seguro de que id sea un string. Si fuera un número, .toUpperCase() explotaría en tiempo de ejecución. 💥
2. Solución: typeof Guards
La forma más común de narrowing es usar el operador typeof de JavaScript dentro de un condicional if. TypeScript entiende este código y “reduce” el tipo dentro del bloque.
function printId(id: string | number) {
if (typeof id === "string") {
// ¡Aquí dentro TypeScript SABE que id es string!
console.log("Tu ID es: " + id.toUpperCase());
} else {
// Y aquí sabe que TIENE que ser number
console.log("Tu ID es: " + id.toFixed(2));
}
}
printId("Pizza!");
printId(123.456);function printId(id: string | number) {
if (typeof id === "string") {
// ¡Aquí dentro TypeScript SABE que id es string!
console.log("Tu ID es: " + id.toUpperCase());
} else {
// Y aquí sabe que TIENE que ser number
console.log("Tu ID es: " + id.toFixed(2));
}
}
printId("Pizza!");
printId(123.456);Tipos soportados por typeof
Recuerda que typeof solo funciona con primitivos básicos: "string", "number", "boolean", "symbol", "undefined", "object" y "function".
¡Ojo! typeof null devuelve "object", lo cual es un bug histórico de JS.
3. Truthiness Narrowing
A veces no necesitas saber el tipo exacto, solo si el valor existe (no es null ni undefined).
function printName(name?: string) {
// name es string | undefined
if (name) {
// Aquí name es string (porque undefined es falsy)
console.log(name.toUpperCase());
}
}function printName(name?: string) {
// name es string | undefined
if (name) {
// Aquí name es string (porque undefined es falsy)
console.log(name.toUpperCase());
}
}4. instanceof Narrowing
Para objetos construidos con clases, typeof no sirve de mucho (todo es “object”). Aquí entra instanceof.
class Perro {
ladrar() { return "Guau!"; }
}
class Gato {
maullar() { return "Miau!"; }
}
function comunicar(mascota: Perro | Gato) {
if (mascota instanceof Perro) {
console.log(mascota.ladrar());
} else {
console.log(mascota.maullar());
}
}
comunicar(new Perro());class Perro {
ladrar() { return "Guau!"; }
}
class Gato {
maullar() { return "Miau!"; }
}
function comunicar(mascota: Perro | Gato) {
if (mascota instanceof Perro) {
console.log(mascota.ladrar());
} else {
console.log(mascota.maullar());
}
}
comunicar(new Perro());5. Predicados de Tipo (Custom Type Guards)
¿Qué pasa si quieres reutilizar una lógica de comprobación compleja? Puedes crear tus propias funciones de guardia usando el operador is.
La sintaxis clave es el tipo de retorno: param is Type.
type Pez = { nadar: () => void };
type Pajaro = { volar: () => void };
// Función guardia personalizada
function esPez(mascota: Pez | Pajaro): mascota is Pez {
return (mascota as Pez).nadar !== undefined;
}
function mover(animal: Pez | Pajaro) {
if (esPez(animal)) {
// TypeScript confía en tu función: aquí es Pez
animal.nadar();
} else {
// Aquí debe ser Pajaro
animal.volar();
}
}
const nemo = { nadar: () => console.log("Nadando...") };
mover(nemo);type Pez = { nadar: () => void };
type Pajaro = { volar: () => void };
// Función guardia personalizada
function esPez(mascota: Pez | Pajaro): mascota is Pez {
return (mascota as Pez).nadar !== undefined;
}
function mover(animal: Pez | Pajaro) {
if (esPez(animal)) {
// TypeScript confía en tu función: aquí es Pez
animal.nadar();
} else {
// Aquí debe ser Pajaro
animal.volar();
}
}
const nemo = { nadar: () => console.log("Nadando...") };
mover(nemo);Poder y Responsabilidad
Cuando usas is, le dices al compilador “confía en mí”. Si tu lógica dentro de la función esPez está mal, TypeScript se lo creerá y podrías tener errores en tiempo de ejecución.
6. Ejercicio: El Formateador Universal
¡Te toca! Tengo una función que recibe un input. Puede ser un string o un number.
- Si es
string: Devuélvelo en MAYÚSCULAS. - Si es
number: Devuélvelo con 2 decimales (usa.toFixed(2)).
El Narrowing es esencial para escribir código que sea flexible (acepte varios tipos) pero seguro (trate cada uno como corresponde). En el siguiente capítulo, veremos qué pasa cuando realmente no sabemos qué tipo nos llega… unknown vs any.