TypeScript puede estrechar tipos automáticamente con typeof, instanceof e in, pero a veces la lógica de validación es demasiado específica para estos operadores. Los type guards personalizados con la sintaxis param is Tipo y las assertion functions con asserts permiten encapsular cualquier lógica de validación y que TypeScript la use para estrechar tipos en el código que llama a esas funciones.
Type guards con is: la sintaxis básica
Una función que devuelve param is Tipo en lugar de boolean actúa como type guard. Cuando la función devuelve true, TypeScript estrecha el tipo de param a Tipo en el bloque where se llama:
interface Articulo {
id: number;
titulo: string;
url: string;
}
function esArticulo(valor: unknown): valor is Articulo {
return (
typeof valor === "object" &&
valor !== null &&
typeof (valor as any).id === "number" &&
typeof (valor as any).titulo === "string" &&
typeof (valor as any).url === "string"
);
}
const datos: unknown = await fetch("/api/articulo/1").then(r => r.json());
if (esArticulo(datos)) {
console.log(datos.titulo); // datos: Articulo, acceso seguro
}
Validar respuestas de API
El caso de uso más habitual es validar datos que llegan de fuentes externas donde el tipo no está garantizado en tiempo de ejecución:
interface RespuestaLista{ datos: T[]; total: number; pagina: number; } function esRespuestaLista ( valor: unknown, esItem: (item: unknown) => item is T ): valor is RespuestaLista { if ( typeof valor !== "object" || valor === null || !Array.isArray((valor as any).datos) || typeof (valor as any).total !== "number" ) return false; return (valor as any).datos.every(esItem); } async function obtenerArticulos(): Promise { const respuesta: unknown = await fetch("/api/articulos").then(r => r.json()); if (!esRespuestaLista(respuesta, esArticulo)) throw new Error("Formato inesperado"); return respuesta.datos; // respuesta: RespuestaLista }
Filtrar arrays de tipo mixto
Los type guards son especialmente útiles con Array.prototype.filter. TypeScript no infiere automáticamente que filtrar por tipo cambia el tipo del array resultado; con un type guard sí:
function esCadena(valor: unknown): valor is string {
return typeof valor === "string";
}
const mixto: (string | number | null)[] = ["a", 1, null, "b", 2];
// Sin type guard: (string | number | null)[]
const sinGuard = mixto.filter(x => typeof x === "string");
// Con type guard: string[]
const soloStrings = mixto.filter(esCadena);
Assertion functions: asserts cond y asserts x is T
Las assertion functions son parecidas a los type guards pero se usan de forma imperativa: si la condición no se cumple, lanzan un error; si el código sigue ejecutándose, TypeScript sabe que la condición es true:
function afirmar(condicion: boolean, mensaje: string): asserts condicion {
if (!condicion) throw new Error(mensaje);
}
function afirmarEsString(valor: unknown): asserts valor is string {
if (typeof valor !== "string") {
throw new TypeError(`Se esperaba string, recibido ${typeof valor}`);
}
}
function procesar(entrada: unknown): string {
afirmarEsString(entrada);
// A partir de aquí, TypeScript sabe que entrada: string
return entrada.toUpperCase();
}
Cuándo usar discriminated unions en su lugar
Los type guards personalizados son más verbosos que los discriminated unions y desplazan la responsabilidad de la corrección a la función guard. Cuando controlas el tipo de los datos (porque los produces tú), los discriminated unions con una propiedad tipo o kind son más simples y no requieren funciones adicionales. Los type guards son la herramienta correcta en las fronteras de la aplicación: cuando recibes datos de una API, un formulario o cualquier fuente externa donde no controlas el formato.
Imagen: Pexels / Mathews Jumba
