TypeScript de Cero a Experto / Narrowing
Type guards propios: la función que enseña a TS
typeof, instanceof e in son guardias de fábrica. Pero puedes escribir los
TUYOS: una función que, cuando devuelve true, le PROMETE a TypeScript "este
valor es de tal tipo" —y TS le cree y estrecha. Se llaman type guards
personalizados, y son la herramienta con la que validas datos del mundo exterior
(un fetch, un JSON.parse) y los conviertes de unknown en tipos seguros.
El inspector certificado
El predicado de tipo: valor is Tipo
Un type guard es una función normal cuyo retorno se anota como parametro is Tipo, en vez de boolean. Cuando devuelve true, TS estrecha:
type Gasto = { nombre: string; valor: number };
// una función booleana normal NO estrecha; con 'x is Gasto' SÍ:
function esGasto(x: unknown): x is Gasto {
return (
typeof x === "object" &&
x !== null &&
"nombre" in x && typeof (x as any).nombre === "string" &&
"valor" in x && typeof (x as any).valor === "number"
);
}
function procesar(dato: unknown): string {
if (esGasto(dato)) {
// 🔍 TS confía en el guard: aquí dato es Gasto, con autocompletado y todo
return dato.nombre + ": $" + dato.valor;
}
return "no es un gasto válido";
}
console.log(procesar({ nombre: "Café", valor: 4000 })); // Café: $4000
console.log(procesar({ foo: 1 })); // no es un gasto válidoLa magia está en el : x is Gasto. Si esa función devolviera : boolean normal,
TS NO estrecharía —seguirías con unknown dentro del if. El predicado is es la
promesa: "confía en mí, si esto es true, x es un Gasto". TS te toma la
palabra. (Nota: el as any dentro del guard es un mal necesario controlado para
inspeccionar campos de un unknown; el guard ENCAPSULA esa comprobación insegura
en un solo lugar confiable.)
Por qué importan: validar el mundo exterior
Los datos que llegan de un fetch, de localStorage o de JSON.parse son
unknown (o deberían serlo, módulo 2). Un type guard es el puente seguro entre ese
"no sé qué es" y tus tipos:
type Usuario = { id: number; nombre: string };
function esUsuario(x: unknown): x is Usuario {
return (
typeof x === "object" && x !== null &&
"id" in x && typeof (x as any).id === "number" &&
"nombre" in x && typeof (x as any).nombre === "string"
);
}
// dato de fuera: no confiamos hasta validar
const crudo: unknown = JSON.parse('{"id": 1, "nombre": "Sara"}');
if (esUsuario(crudo)) {
// 🔍 aquí crudo es Usuario: seguro de verdad, validado en runtime
console.log("Bienvenida, " + crudo.nombre);
} else {
console.log("Datos inválidos");
}Fíjate en el crudos.filter(esProducto): como esProducto es un type guard, TS
sabe que el resultado es Producto[] —no unknown[]—, y dentro del bucle accedes
a .nombre y .precio con total seguridad. Un guard bien escrito no solo valida:
transforma un tipo dudoso en uno confiable para todo lo que venga después.
Escribes function esValido(x: unknown): boolean { return typeof x === 'object' && x !== null }. Lo usas en if (esValido(dato)) { dato.nombre }. ¿Por qué TS sigue marcando error en dato.nombre?
Mini-reto
Certifica datos: 1) define type Punto = { x: number; y: number }; 2) escribe el
type guard esPunto(v: unknown): v is Punto que valide que es un objeto con x e
y numéricos; 3) úsalo para filtrar un array unknown[] con puntos válidos e
inválidos mezclados, y confirma que TS trata el resultado como Punto[]. Acabas de
construir el puente seguro entre el mundo exterior y tus tipos.
Qué sigue
Los guards por propiedad (in) funcionan, pero dependen de recordar qué campo es
único de cada tipo. La próxima lección presenta la forma más elegante y robusta de
modelar "uno de varios casos": las DISCRIMINATED UNIONS —para muchos, el patrón más
útil de todo TypeScript.