Skip to content

Narrowing ​

Cuando una variable puede ser varias cosas (string | number, Usuario | null, una respuesta exitosa o un error), TypeScript no te deja usarla libremente hasta que compruebes cuál es. A ese proceso de reducir un tipo amplio a uno más específico se le llama narrowing (estrechamiento). Es la técnica que más usarás para escribir código seguro.

Concepto ​

Piensa en un guardia de seguridad en la entrada de un edificio. Antes de la puerta, "alguien" puede ser empleado, visitante o repartidor. Después de revisar su gafete, el guardia sabe quién es y lo deja pasar al área correcta. TypeScript hace lo mismo: después de un if, sabe qué tipo hay dentro de ese bloque.

ts
function imprimirId(id: string | number) {
  // Aquí id es string | number
  if (typeof id === 'string') {
    // Aquí TS sabe que id es string
    console.log(id.toUpperCase());
  } else {
    // Y aquí, que es number
    console.log(id.toFixed(0));
  }
}

TypeScript analiza el flujo de control de tu código: if, return, switch, &&, ??... y ajusta el tipo en cada punto.

typeof ​

Sirve para primitivos: 'string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'function' y 'object'.

ts
function formatear(valor: string | number | boolean): string {
  if (typeof valor === 'string') return valor.trim();
  if (typeof valor === 'number') return valor.toLocaleString('es-MX');
  return valor ? 'Sí' : 'No';  // aquí solo queda boolean
}

Error común

typeof null es 'object' (un error histórico de JavaScript). Por eso typeof x === 'object' no descarta null: TS te seguirá avisando que x puede ser null. Compruébalo aparte: if (x !== null && typeof x === 'object').

Truthiness ​

Un if (valor) descarta null, undefined, '', 0, NaN y false:

ts
function saludar(nombre: string | null | undefined) {
  if (nombre) {
    return `Hola, ${nombre}`;  // nombre: string
  }
  return 'Hola, invitado';
}

// Con && y ?. también funciona
const largo = usuarioActual && usuarioActual.nombre.length;

Cuidado: si 0 o '' son valores válidos (una cantidad, un campo vacío intencional), un if (valor) los descartará por error. En esos casos compara explícitamente con null/undefined o usa ??.

ts
function mostrarStock(stock: number | undefined) {
  if (stock) return `${stock} piezas`;         // ❌ con 0 dice "Sin dato"
  return 'Sin dato';
}

function mostrarStockBien(stock: number | undefined) {
  if (stock !== undefined) return `${stock} piezas`; // ✅ 0 es válido
  return 'Sin dato';
}

Igualdad ​

Comparar con === o !== también estrecha, sobre todo con literales y null:

ts
type Metodo = 'GET' | 'POST' | 'PUT' | 'DELETE';

function tieneCuerpo(metodo: Metodo): boolean {
  if (metodo === 'GET' || metodo === 'DELETE') {
    return false;      // metodo: 'GET' | 'DELETE'
  }
  return true;         // metodo: 'POST' | 'PUT'
}

function procesar(valor: string | null) {
  if (valor === null) return;
  console.log(valor.length);   // valor: string
}

instanceof ​

Para clases y objetos construidos con new (fechas, errores, elementos del DOM):

ts
function describirFecha(fecha: Date | string): string {
  if (fecha instanceof Date) {
    return fecha.toLocaleDateString('es-MX');
  }
  return new Date(fecha).toLocaleDateString('es-MX');
}

// Muy común en catch, donde el error es unknown
try {
  JSON.parse('{mal json');
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error('JSON inválido:', error.message);
  } else if (error instanceof Error) {
    console.error(error.message);
  }
}

in ​

El operador in comprueba si un objeto tiene cierta propiedad. Útil para distinguir objetos de formas distintas:

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

function hacerSonido(animal: Perro | Gato) {
  if ('ladrar' in animal) {
    animal.ladrar();   // animal: Perro
  } else {
    animal.maullar();  // animal: Gato
  }
}

Uniones discriminadas ​

Es el patrón más útil de esta lección. Cada miembro de la unión tiene una propiedad común con un valor literal distinto (el "discriminante", normalmente tipo, kind o estado). Revisando esa propiedad, TS sabe exactamente qué forma tiene el objeto.

ts
// Estado de una petición: cada caso tiene datos distintos
type EstadoPeticion<T> =
  | { estado: 'inactivo' }
  | { estado: 'cargando' }
  | { estado: 'exito'; datos: T }
  | { estado: 'error'; mensaje: string };

interface Producto { id: number; nombre: string }

function render(e: EstadoPeticion<Producto[]>): string {
  switch (e.estado) {
    case 'inactivo':
      return 'Presiona buscar';
    case 'cargando':
      return 'Cargando...';
    case 'exito':
      return e.datos.map((p) => p.nombre).join(', ');  // datos existe aquí
    case 'error':
      return `Error: ${e.mensaje}`;                    // mensaje existe aquí
  }
}

Compáralo con la alternativa "todo opcional" ({ cargando: boolean; datos?: T; error?: string }), que permite estados absurdos como "cargando con error y con datos a la vez". La unión discriminada hace imposibles los estados imposibles.

Otro ejemplo real: métodos de pago.

ts
type Pago =
  | { metodo: 'tarjeta'; ultimos4: string; marca: 'visa' | 'mastercard' }
  | { metodo: 'oxxo'; referencia: string; vence: Date }
  | { metodo: 'transferencia'; clabe: string };

function resumenPago(p: Pago): string {
  if (p.metodo === 'tarjeta') return `${p.marca} terminación ${p.ultimos4}`;
  if (p.metodo === 'oxxo') return `Paga en OXXO con ref. ${p.referencia}`;
  return `Transferencia a CLABE ${p.clabe}`;
}

Exhaustividad con never ​

¿Qué pasa si mañana agregas { metodo: 'paypal'; correo: string } a Pago? La función resumenPago devolvería el texto de transferencia por error. Para que TS te obligue a manejar todos los casos, usa never:

ts
function asegurarNunca(valor: never): never {
  throw new Error(`Caso no manejado: ${JSON.stringify(valor)}`);
}

function iconoPago(p: Pago): string {
  switch (p.metodo) {
    case 'tarjeta':
      return '💳';
    case 'oxxo':
      return '🏪';
    case 'transferencia':
      return '🏦';
    default:
      // Si cubriste todo, p es never y esto compila.
      // Si agregas 'paypal' y no lo manejas, p es { metodo: 'paypal' ... }
      // y TS marca error aquí.
      return asegurarNunca(p);
  }
}

Buena práctica

Usa uniones discriminadas + switch + asegurarNunca para estados de UI, eventos, acciones de reducers y respuestas de API. Cuando agregues un caso nuevo, TS te llevará de la mano a cada lugar que debes actualizar.

Type guards con is ​

Cuando la comprobación es compleja, la encapsulas en una función que devuelve valor is Tipo. A esto se le llama type guard (guardia de tipo) o predicado de tipo:

ts
interface Usuario {
  id: number;
  nombre: string;
  email: string;
}

// Si devuelve true, TS trata el valor como Usuario
function esUsuario(valor: unknown): valor is Usuario {
  return (
    typeof valor === 'object' &&
    valor !== null &&
    'id' in valor && typeof valor.id === 'number' &&
    'nombre' in valor && typeof valor.nombre === 'string' &&
    'email' in valor && typeof valor.email === 'string'
  );
}

const datos: unknown = JSON.parse('{"id":1,"nombre":"Ana","email":"a@a.mx"}');

if (esUsuario(datos)) {
  console.log(datos.email);   // datos: Usuario
}

// Con filter: el resultado queda bien tipado
const mezcla: (Usuario | null)[] = [null, { id: 1, nombre: 'Ana', email: 'a@a.mx' }];
const usuarios = mezcla.filter((u): u is Usuario => u !== null);  // Usuario[]

Error común

TS confía en tu type guard. Si la función dice valor is Usuario pero solo revisa el id, TS creerá que también hay email aunque no exista. Revisa todas las propiedades que vas a usar.

Funciones asserts ​

Una variante: en lugar de devolver boolean, la función lanza un error si la condición no se cumple. Después de llamarla, TS asume que la condición es verdadera:

ts
function asegurar(condicion: unknown, mensaje: string): asserts condicion {
  if (!condicion) throw new Error(mensaje);
}

function asegurarUsuario(valor: unknown): asserts valor is Usuario {
  if (!esUsuario(valor)) throw new Error('Datos de usuario inválidos');
}

const contenedor = document.querySelector('#app');
asegurar(contenedor, 'No existe #app');
contenedor.innerHTML = 'Listo';   // contenedor ya no es null

const respuesta: unknown = await (await fetch('/api/yo')).json();
asegurarUsuario(respuesta);
console.log(respuesta.nombre);   // respuesta: Usuario

Validación de datos externos ​

Recuerda: los tipos desaparecen al ejecutar. Cualquier dato que venga de fuera (APIs, formularios, localStorage, parámetros de URL) puede no tener la forma que esperas. Los type guards a mano funcionan, pero se vuelven largos. Por eso en proyectos reales se usan librerías de validación como Zod, que validan en tiempo de ejecución y generan el tipo automáticamente:

ts
import { z } from 'zod';

// Defines el esquema una sola vez
const UsuarioSchema = z.object({
  id: z.number(),
  nombre: z.string().min(1),
  email: z.string().email(),
  rol: z.enum(['admin', 'cliente'])
});

// El tipo se deriva del esquema: no se repite nada
type Usuario = z.infer<typeof UsuarioSchema>;

async function obtenerUsuario(id: number): Promise<Usuario> {
  const res = await fetch(`/api/usuarios/${id}`);
  const json: unknown = await res.json();
  return UsuarioSchema.parse(json);  // lanza error si no cumple el esquema
}

// safeParse no lanza: devuelve una unión discriminada
const resultado = UsuarioSchema.safeParse({ id: 'uno' });
if (!resultado.success) {
  console.log(resultado.error.issues);  // detalles de qué falló
} else {
  console.log(resultado.data.nombre);
}

Se instala con npm i zod. Alternativas similares: Valibot, ArkType y Yup.

Ejercicios ​

  1. Escribe duplicar(valor: string | number) que repita el texto dos veces o multiplique el número por 2.
  2. Define una unión discriminada Figura con circulo (radio), rectangulo (ancho, alto) y triangulo (base, altura), y una función area con switch y comprobación de exhaustividad.
  3. Escribe un type guard esProducto(valor: unknown): valor is Producto para interface Producto { nombre: string; precio: number }.
  4. ¿Qué problema tiene este código? Corrígelo.
ts
function descuento(porcentaje?: number) {
  const p = porcentaje || 10;
  return p;
}
descuento(0);
Ver solución
ts
// 1
function duplicar(valor: string | number): string | number {
  return typeof valor === 'string' ? valor.repeat(2) : valor * 2;
}

// 2
type Figura =
  | { tipo: 'circulo'; radio: number }
  | { tipo: 'rectangulo'; ancho: number; alto: number }
  | { tipo: 'triangulo'; base: number; altura: number };

function asegurarNunca(x: never): never {
  throw new Error(`Caso no manejado: ${JSON.stringify(x)}`);
}

function area(f: Figura): number {
  switch (f.tipo) {
    case 'circulo':
      return Math.PI * f.radio ** 2;
    case 'rectangulo':
      return f.ancho * f.alto;
    case 'triangulo':
      return (f.base * f.altura) / 2;
    default:
      return asegurarNunca(f);
  }
}

// 3
interface Producto { nombre: string; precio: number }

function esProducto(valor: unknown): valor is Producto {
  return (
    typeof valor === 'object' &&
    valor !== null &&
    'nombre' in valor && typeof valor.nombre === 'string' &&
    'precio' in valor && typeof valor.precio === 'number'
  );
}

// 4: con || el 0 se considera falsy y se reemplaza por 10.
// Usa ?? que solo reemplaza null y undefined:
function descuento(porcentaje?: number) {
  return porcentaje ?? 10;
}
descuento(0); // 0

Resumen ​

  • Narrowing es reducir un tipo amplio a uno específico mediante comprobaciones.
  • typeof para primitivos, instanceof para clases, in para propiedades, === para literales y null.
  • Cuidado con truthiness cuando 0 o '' son valores válidos.
  • Las uniones discriminadas modelan estados sin combinaciones imposibles.
  • never + default garantiza que manejes todos los casos.
  • Los type guards (valor is Tipo) y asserts encapsulan validaciones.
  • Para datos externos valida en tiempo de ejecución; Zod genera el tipo desde el esquema.

Siguiente: Módulos y DOM

Academia Web — aprende, copia y construye.