Skip to content

Lección 97: Type Guards y Type Narrowing

🎯 Objetivo: Dominar las técnicas de narrowing de tipos en TypeScript: typeof, instanceof, in, type predicates, discriminated unions y assertion functions.


Type narrowing es el proceso de reducir un tipo amplio a un tipo más específico dentro de un bloque de código.

💻 Ejemplo

function procesar(valor: string | number | boolean): void {
if (typeof valor === "string") {
console.log(valor.toUpperCase()); // ✅ string
} else if (typeof valor === "number") {
console.log(valor.toFixed(2)); // ✅ number
} else {
console.log(valor ? "true" : "false"); // ✅ boolean
}
}

⚠️ typeof funciona con primitivos: string, number, boolean, symbol, bigint, undefined, function. Para objetos, devuelve "object" (incluyendo null).

class Perro {
ladrar(): void { console.log("Guau"); }
}
class Gato {
maullar(): void { console.log("Miau"); }
}
type Mascota = Perro | Gato;
function hacerSonido(mascota: Mascota): void {
if (mascota instanceof Perro) {
mascota.ladrar(); // ✅ acotado a Perro
} else {
mascota.maullar(); // ✅ acotado a Gato
}
}
type Usuario = { nombre: string; email: string };
type Admin = { nombre: string; email: string; rol: "admin" | "superadmin" };
function esAdmin(persona: Usuario | Admin): boolean {
return "rol" in persona;
}
function getInfo(persona: Usuario | Admin): string {
if ("rol" in persona) {
return `Admin ${persona.nombre} (${persona.rol})`;
}
return `Usuario ${persona.nombre}`;
}

Una type predicate es una función que devuelve un booleano y asegura a TypeScript que el valor es de un tipo específico.

interface Pez {
nadar(): void;
tipo: "agua dulce" | "agua salada";
}
interface Ave {
volar(): void;
tipo: "terrestre" | "marina";
}
type Animal = Pez | Ave;
function esPez(animal: Animal): animal is Pez {
return (animal as Pez).nadar !== undefined;
}
function mover(animal: Animal): void {
if (esPez(animal)) {
animal.nadar(); // ✅ TypeScript sabe que es Pez
} else {
animal.volar(); // ✅ Es Ave
}
}
interface ErrorAPI {
codigo: number;
mensaje: string;
error: true;
}
interface ExitoAPI<T> {
datos: T;
error: false;
}
type RespuestaAPI<T> = ErrorAPI | ExitoAPI<T>;
function esError<T>(respuesta: RespuestaAPI<T>): respuesta is ErrorAPI {
return respuesta.error === true;
}
function manejarRespuesta<T>(respuesta: RespuestaAPI<T>): void {
if (esError(respuesta)) {
console.error(`Error ${respuesta.codigo}: ${respuesta.mensaje}`);
} else {
console.log("Datos:", respuesta.datos);
}
}

Una unión discriminada tiene una propiedad literal común (el discriminante) que permite narrowing por switch/if.

type EstadoPedido =
| { tipo: "pendiente"; fechaCreacion: Date }
| { tipo: "enviado"; fechaEnvio: Date; tracking: string }
| { tipo: "entregado"; fechaEntrega: Date; firmadoPor: string }
| { tipo: "cancelado"; fechaCancelacion: Date; motivo: string };
function procesarPedido(pedido: EstadoPedido): string {
switch (pedido.tipo) {
case "pendiente":
return `Pedido creado el ${pedido.fechaCreacion.toLocaleDateString()}`;
case "enviado":
return `Enviado el ${pedido.fechaEnvio.toLocaleDateString()}. Tracking: ${pedido.tracking}`;
case "entregado":
return `Entregado el ${pedido.fechaEntrega.toLocaleDateString()}. Recibido por: ${pedido.firmadoPor}`;
case "cancelado":
return `Cancelado: ${pedido.motivo}`;
default:
const _exhaustivo: never = pedido;
return _exhaustivo;
}
}

🧠 El default con never es un exhaustiveness check: si agregas un nuevo tipo al discriminated union, el compilador lo señalará.

type EventoUI =
| { tipo: "click"; x: number; y: number }
| { tipo: "keypress"; tecla: string; ctrlKey: boolean }
| { tipo: "submit"; formId: string };
function manejarEvento(evento: EventoUI): void {
switch (evento.tipo) {
case "click":
console.log(`Click en (${evento.x}, ${evento.y})`);
break;
case "keypress":
if (evento.ctrlKey) {
console.log(`Ctrl+${evento.tecla}`);
}
break;
case "submit":
console.log(`Formulario ${evento.formId} enviado`);
break;
}
}

Las assertion functions son funciones que lanzan un error si la condición no se cumple, y le indican a TypeScript que el tipo se ha reducido.

function assertString(valor: unknown): asserts valor is string {
if (typeof valor !== "string") {
throw new Error("Se esperaba un string");
}
}
function procesar(valor: unknown): void {
assertString(valor);
console.log(valor.toUpperCase()); // ✅ TypeScript sabe que es string
}
function assertNonNull<T>(valor: T): asserts valor is NonNullable<T> {
if (valor === null || valor === undefined) {
throw new Error("El valor no debe ser null/undefined");
}
}
function obtenerNombre(): string | null {
return Math.random() > 0.5 ? "Ana" : null;
}
const nombre = obtenerNombre();
assertNonNull(nombre);
console.log(nombre.toUpperCase()); // ✅ ahora es string
type UsuarioValido = { nombre: string; email: string };
type UsuarioInvalido = { error: string };
type ResultadoUsuario = UsuarioValido | UsuarioInvalido;
function esUsuarioValido(usuario: ResultadoUsuario): asserts usuario is UsuarioValido {
if ("error" in usuario) {
throw new Error(usuario.error);
}
}
function usarUsuario(usuario: ResultadoUsuario): void {
esUsuarioValido(usuario);
console.log(usuario.nombre, usuario.email); // ✅
}

TécnicaSintaxisCuándo usarla
typeoftypeof x === "string"Primitivos
instanceofx instanceof ClaseInstancias de clase
in"prop" in xPropiedades opcionales
Type predicatex is TipoLógica personalizada
Discriminated unionx.tipo === "algo"Objetos con discriminante
Assertion functionasserts x is TipoValidación temprana

📝 Resumen: Type narrowing permite que TypeScript reduzca tipos amplios a específicos dentro de bloques de código. Las discriminated unions con switch son el patrón más potente para flujos de estado. Los type predicates y assertion functions permiten lógica personalizada de narrowing. Siempre busca tener un never exhaustivo para mantener el código a prueba de cambios.