CodeForge

TypeScript de Cero a Experto / Narrowing

Type guards propios: la función que enseña a TS

Teoría20 min20 XP

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-guard.ts
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álido

La 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:

validar-externo.ts
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.