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.
1. Type Narrowing básico
Section titled “1. Type Narrowing básico”Type narrowing es el proceso de reducir un tipo amplio a un tipo más específico dentro de un bloque de código.
typeof
Section titled “typeof”💻 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).
instanceof
Section titled “instanceof”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 }}in — verificar propiedades
Section titled “in — verificar propiedades”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}`;}2. Type Predicates valor is Tipo
Section titled “2. Type Predicates valor is Tipo”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 }}Type predicate más complejo
Section titled “Type predicate más complejo”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); }}3. Discriminated Unions
Section titled “3. Discriminated Unions”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á.
Ejemplo con eventos UI
Section titled “Ejemplo con eventos UI”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; }}4. Assertion Functions
Section titled “4. Assertion Functions”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}Assertion function con condición
Section titled “Assertion function con condición”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 stringAssertion function con forma booleana
Section titled “Assertion function con forma booleana”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); // ✅}5. Comparativa de técnicas
Section titled “5. Comparativa de técnicas”| Técnica | Sintaxis | Cuándo usarla |
|---|---|---|
typeof | typeof x === "string" | Primitivos |
instanceof | x instanceof Clase | Instancias de clase |
in | "prop" in x | Propiedades opcionales |
| Type predicate | x is Tipo | Lógica personalizada |
| Discriminated union | x.tipo === "algo" | Objetos con discriminante |
| Assertion function | asserts x is Tipo | Validació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.