Narrowing en TypeScript: typeof, instanceof, in y discriminated unions

El narrowing es uno de los mecanismos más potentes de TypeScript: el compilador analiza el flujo de ejecución y reduce automáticamente el tipo de una variable en cada rama. No necesitas aserciones manuales ni castings; TypeScript lo hace solo cuando le das las pistas correctas. Esta guía cubre los guards más habituales y el patrón de discriminated unions.

typeof: distinguir primitivos

typeof es el guard más simple. TypeScript lo entiende y en cada rama conoce el tipo exacto de la variable:

function procesar(valor: string | number | boolean): string {
  if (typeof valor === "string") {
    return valor.toUpperCase(); // valor: string
  }
  if (typeof valor === "number") {
    return valor.toFixed(2);   // valor: number
  }
  return valor ? "sí" : "no"; // valor: boolean
}

typeof funciona bien con primitivos pero tiene un problema conocido: typeof null === "object". Para null, usa una comparación de igualdad estricta (valor === null).

instanceof: comprobar clases

instanceof comprueba si un valor es instancia de una clase concreta. TypeScript lo usa para narrowing cuando la variable puede ser de varios tipos de clase:

class ErrorRed extends Error {
  constructor(public codigo: number, mensaje: string) {
    super(mensaje);
  }
}

class ErrorValidacion extends Error {
  constructor(public campo: string, mensaje: string) {
    super(mensaje);
  }
}

function manejarError(e: ErrorRed | ErrorValidacion): void {
  if (e instanceof ErrorRed) {
    console.error(`Error de red ${e.codigo}: ${e.message}`);
  } else {
    console.error(`Campo inválido ${e.campo}: ${e.message}`);
  }
}

El operador in: comprobar propiedades

in comprueba si una propiedad existe en un objeto. Es útil para discriminar tipos que no tienen una propiedad discriminante explícita con literales:

interface Perro { nombre: string; ladrar(): void }
interface Gato  { nombre: string; maullar(): void }

function hablar(animal: Perro | Gato): void {
  if ("ladrar" in animal) {
    animal.ladrar(); // animal: Perro
  } else {
    animal.maullar(); // animal: Gato
  }
}

Type guards personalizados con is

Cuando ninguno de los guards anteriores sirve, puedes escribir tu propia función de comprobación. El tipo de retorno param is Tipo le dice a TypeScript que si la función devuelve true, el parámetro es de ese tipo:

interface Articulo {
  id: number;
  titulo: string;
  url: string;
}

function esArticulo(valor: unknown): valor is Articulo {
  return (
    typeof valor === "object" &&
    valor !== null &&
    "id" in valor &&
    "titulo" in valor &&
    "url" in valor
  );
}

const respuesta: unknown = await fetch("/api/articulo/1").then(r => r.json());

if (esArticulo(respuesta)) {
  console.log(respuesta.titulo); // respuesta: Articulo
}

Discriminated unions: el narrowing más robusto

El patrón más limpio para narrowing complejo es el discriminated union: cada variante del tipo tiene una propiedad literal única. TypeScript lo resuelve con un switch sin necesidad de guards adicionales:

type Evento =
  | { tipo: "click"; x: number; y: number }
  | { tipo: "tecla"; codigo: string; ctrl: boolean }
  | { tipo: "scroll"; delta: number };

function procesarEvento(ev: Evento): void {
  switch (ev.tipo) {
    case "click":
      console.log(`Click en (${ev.x}, ${ev.y})`);
      break;
    case "tecla":
      console.log(`Tecla ${ev.codigo}${ev.ctrl ? " + Ctrl" : ""}`);
      break;
    case "scroll":
      console.log(`Scroll ${ev.delta}px`);
      break;
  }
}

En cada case, TypeScript sabe exactamente qué propiedades existen. No hay riesgo de acceder a ev.x en el case de "tecla": el compilador lo detectaría como error.

El narrowing bien aplicado elimina la necesidad de castings y type assertions en la mayor parte del código de aplicación. Cuando te veas escribiendo as NombreTipo, plantéate si un type guard o un discriminated union puede resolver el problema sin la aserción manual.

Imagen: Pexels / Pixabay

COMPARTE ESTE ARTÍCULO

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