Type Guards personalizados en TypeScript: funciones is y asserts

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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP