Type guards y narrowing

Intermedio
TypeScript
TypeScript
Actualizado: 17/04/2026

Guards con typeof, instanceof e in

Los type guards son expresiones que TypeScript reconoce como comprobaciones de tipo. Cuando una condición actúa como type guard, TypeScript estrecha (narrows) el tipo de la variable dentro del bloque correspondiente, proporcionando acceso seguro a propiedades y métodos específicos.

Flujo de narrowing con type guards

typeof guards

El operador typeof permite distinguir entre tipos primitivos. TypeScript reconoce las comprobaciones typeof y ajusta el tipo automáticamente:

function formatear(valor: string | number | boolean): string {
  if (typeof valor === "string") {
    return valor.toUpperCase()
  }
  if (typeof valor === "number") {
    return valor.toFixed(2)
  }
  return valor ? "verdadero" : "falso"
}

console.log(formatear("hola"))  // "HOLA"
console.log(formatear(3.14159)) // "3.14"
console.log(formatear(true))    // "verdadero"

Las cadenas válidas que devuelve typeof son: "string", "number", "boolean", "undefined", "object", "function", "symbol" y "bigint":

function describir(valor: unknown): string {
  if (typeof valor === "string") return `Cadena de ${valor.length} caracteres`
  if (typeof valor === "number") return `Número: ${valor}`
  if (typeof valor === "boolean") return `Booleano: ${valor}`
  if (typeof valor === "undefined") return "Sin valor"
  if (typeof valor === "function") return "Es una función"
  if (typeof valor === "symbol") return "Es un simbolo"
  if (typeof valor === "bigint") return `BigInt: ${valor}`
  return "Es un objeto"
}

El operador typeof devuelve "object" tanto para objetos como para null y arrays. Para distinguir estos casos se necesitan comprobaciones adicionales como Array.isArray() o comparación con null.

instanceof guards

El operador instanceof verifica si un objeto fue creado a partir de un constructor específico. TypeScript estrecha el tipo a la clase correspondiente:

class ErrorValidacion {
  constructor(public campo: string, public mensaje: string) {}
}

class ErrorAutenticacion {
  constructor(public motivo: string, public intentos: number) {}
}

class ErrorRed {
  constructor(public url: string, public código: number) {}
}

type ErrorApp = ErrorValidacion | ErrorAutenticacion | ErrorRed

function manejarError(error: ErrorApp): string {
  if (error instanceof ErrorValidacion) {
    return `Validación fallida en '${error.campo}': ${error.mensaje}`
  }
  if (error instanceof ErrorAutenticacion) {
    return `Autenticacion fallida: ${error.motivo} (${error.intentos} intentos)`
  }
  if (error instanceof ErrorRed) {
    return `Error de red ${error.código} en ${error.url}`
  }
  return "Error desconocido"
}

const err = new ErrorValidacion("email", "Formato inválido")
console.log(manejarError(err)) // "Validación fallida en 'email': Formato inválido"

El instanceof también detecta la herencia, estrechando al tipo más específico:

class Vehiculo {
  constructor(public marca: string) {}
}

class Coche extends Vehiculo {
  constructor(marca: string, public puertas: number) {
    super(marca)
  }
}

class Moto extends Vehiculo {
  constructor(marca: string, public cilindrada: number) {
    super(marca)
  }
}

function describir(vehiculo: Vehiculo): string {
  if (vehiculo instanceof Coche) {
    return `Coche ${vehiculo.marca} con ${vehiculo.puertas} puertas`
  }
  if (vehiculo instanceof Moto) {
    return `Moto ${vehiculo.marca} de ${vehiculo.cilindrada}cc`
  }
  return `Vehiculo: ${vehiculo.marca}`
}

in operator guards

El operador in comprueba si una propiedad existe en un objeto. TypeScript lo usa para estrechar uniones basándose en propiedades exclusivas de cada variante:

type Articulo = {
  titulo: string
  contenido: string
  autor: string
}

type Video = {
  titulo: string
  duracion: number
  resolución: string
}

type Podcast = {
  titulo: string
  duracion: number
  episodio: number
}

type Contenido = Articulo | Video | Podcast

function mostrar(contenido: Contenido): string {
  if ("contenido" in contenido) {
    return `Articulo: ${contenido.titulo} por ${contenido.autor}`
  }
  if ("resolución" in contenido) {
    return `Video: ${contenido.titulo} (${contenido.resolución}, ${contenido.duracion}s)`
  }
  return `Podcast ep.${contenido.episodio}: ${contenido.titulo}`
}

console.log(mostrar({ titulo: "TypeScript", contenido: "...", autor: "Ana" }))
console.log(mostrar({ titulo: "Demo", duracion: 120, resolución: "1080p" }))
console.log(mostrar({ titulo: "Entrevista", duracion: 3600, episodio: 42 }))

El operador in es especialmente útil cuando no se dispone de un campo discriminante explícito y los tipos se distinguen por la presencia o ausencia de ciertas propiedades.

Equality narrowing y truthiness narrowing

Equality narrowing

TypeScript estrecha tipos mediante comparaciones de igualdad con ===, !==, == y !=:

function comparar(a: string | number, b: string | boolean): string {
  if (a === b) {
    // Solo string es comun a ambos tipos
    // TypeScript estrecha a: string, b: string
    return a.toUpperCase() + " = " + b.toUpperCase()
  }
  return "No son iguales o tipos diferentes"
}

La comparación con null usando == es especialmente práctica porque cubre tanto null como undefined:

function procesar(valor: string | null | undefined): string {
  if (valor == null) {
    // Cubre tanto null como undefined
    return "Sin valor"
  }
  // TypeScript sabe que valor es string
  return valor.trim()
}

console.log(procesar(null))      // "Sin valor"
console.log(procesar(undefined)) // "Sin valor"
console.log(procesar("  hola ")) // "hola"

La comparación con valores literales específicos también estrecha el tipo:

type Resultado = "exito" | "error" | "pendiente"

function iconoResultado(resultado: Resultado): string {
  if (resultado === "exito") {
    return "[OK]"
  }
  if (resultado === "error") {
    return "[X]"
  }
  // TypeScript sabe que resultado es "pendiente"
  return "[...]"
}

Truthiness narrowing

JavaScript trata ciertos valores como falsy: false, 0, "", null, undefined, NaN. TypeScript aprovecha las comprobaciones de truthiness para eliminar estos valores del tipo:

function saludar(nombre: string | null | undefined): string {
  if (nombre) {
    // Elimina null, undefined y "" (cadena vacia)
    return `Hola, ${nombre}`
  }
  return "Hola, visitante"
}

Es útil para manejar propiedades opcionales de forma concisa:

type ConfigApp = {
  titulo: string
  subtitulo?: string
  maxResultados?: number
  debug?: boolean
}

function aplicarConfig(config: ConfigApp): string[] {
  const lineas: string[] = [`Titulo: ${config.titulo}`]

  if (config.subtitulo) {
    lineas.push(`Subtitulo: ${config.subtitulo}`)
  }

  if (config.maxResultados) {
    lineas.push(`Max resultados: ${config.maxResultados}`)
  }

  // Cuidado: config.debug podría ser false (falsy pero válido)
  if (config.debug !== undefined) {
    lineas.push(`Debug: ${config.debug}`)
  }

  return lineas
}

El truthiness narrowing puede producir errores sutiles con valores como 0, "" o false, que son falsy pero legítimos. Para estos casos es preferible usar comprobaciones explícitas con !== undefined o !== null.

La negación con ! filtra en la rama opuesta:

function procesarLista(items: string[] | null): number {
  if (!items) {
    return 0
  }
  // TypeScript sabe que items es string[]
  return items.length
}

Type predicates y assertion functions

Type predicates con is

Un type predicate es una función que devuelve un booleano y utiliza la sintaxis parámetro is Tipo en su tipo de retorno. Cuando la función devuelve true, TypeScript estrecha el tipo del argumento:

interface Pez {
  nadar(): void
  nombre: string
}

interface Ave {
  volar(): void
  nombre: string
}

function esPez(animal: Pez | Ave): animal is Pez {
  return "nadar" in animal
}

function moverAnimal(animal: Pez | Ave): string {
  if (esPez(animal)) {
    animal.nadar()
    return `${animal.nombre} está nadando`
  }
  animal.volar()
  return `${animal.nombre} está volando`
}

Los type predicates son especialmente útiles para validar datos de fuentes externas:

interface UsuarioAPI {
  id: number
  nombre: string
  email: string
  rol: "admin" | "usuario"
}

function esUsuarioAPI(datos: unknown): datos is UsuarioAPI {
  if (typeof datos !== "object" || datos === null) return false
  const obj = datos as Record<string, unknown>
  return (
    typeof obj.id === "number" &&
    typeof obj.nombre === "string" &&
    typeof obj.email === "string" &&
    (obj.rol === "admin" || obj.rol === "usuario")
  )
}

function procesarDatosAPI(datos: unknown): string {
  if (esUsuarioAPI(datos)) {
    return `Usuario ${datos.nombre} (${datos.rol}): ${datos.email}`
  }
  return "Datos invalidos"
}

const datosValidos = { id: 1, nombre: "Ana", email: "ana@ej.com", rol: "admin" }
const datosInvalidos = { id: "abc", nombre: 123 }

console.log(procesarDatosAPI(datosValidos))   // "Usuario Ana (admin): ana@ej.com"
console.log(procesarDatosAPI(datosInvalidos)) // "Datos invalidos"

Los type predicates también funcionan con arrays para filtrar elementos:

type Resultado = { tipo: "exito"; valor: number } | { tipo: "error"; mensaje: string }

function esExito(r: Resultado): r is { tipo: "exito"; valor: number } {
  return r.tipo === "exito"
}

const resultados: Resultado[] = [
  { tipo: "exito", valor: 42 },
  { tipo: "error", mensaje: "fallo" },
  { tipo: "exito", valor: 100 },
  { tipo: "error", mensaje: "timeout" }
]

const exitosos = resultados.filter(esExito)
// Tipo: { tipo: "exito"; valor: number }[]
console.log(exitosos.map(r => r.valor)) // [42, 100]

Assertion functions con asserts

Las assertion functions son funciones que lanzan un error si la condición no se cumple. Usan la sintaxis asserts parámetro is Tipo:

function assertEsString(valor: unknown): asserts valor is string {
  if (typeof valor !== "string") {
    throw new Error(`Se esperaba string, se recibio ${typeof valor}`)
  }
}

function assertEsNumeroPositivo(valor: unknown): asserts valor is number {
  if (typeof valor !== "number" || valor <= 0) {
    throw new Error(`Se esperaba un número positivo, se recibio ${valor}`)
  }
}

function procesarEntrada(nombre: unknown, edad: unknown): string {
  assertEsString(nombre)
  assertEsNumeroPositivo(edad)
  // Después de las aserciones, TypeScript sabe los tipos
  return `${nombre.toUpperCase()} tiene ${edad.toFixed(0)} anios`
}

console.log(procesarEntrada("Ana", 28)) // "ANA tiene 28 anios"
// procesarEntrada(123, "abc") // Lanza Error

Las assertion functions también pueden usarse sin especificar un tipo, solo confirmando una condición:

function assertDefined<T>(valor: T | null | undefined, nombre: string): asserts valor is T {
  if (valor === null || valor === undefined) {
    throw new Error(`${nombre} no puede ser null o undefined`)
  }
}

function obtenerConfig(clave: string): string | undefined {
  const configs: Record<string, string> = { db: "postgres://localhost" }
  return configs[clave]
}

const dbUrl = obtenerConfig("db")
assertDefined(dbUrl, "DB_URL")
// TypeScript sabe que dbUrl es string
console.log(dbUrl.toUpperCase())

Análisis de control de flujo

TypeScript realiza un análisis de control de flujo (control flow analysis) que rastrea el tipo de cada variable en cada punto del código. Este análisis va más allá de simples comprobaciones: interpreta retornos tempranos, asignaciones y ramificaciones:

function procesar(valor: string | number | null): string {
  // Aquí: string | number | null
  
  if (valor === null) {
    return "Sin valor"
  }
  // Aquí: string | number (null eliminado por el return)
  
  if (typeof valor === "number") {
    return valor.toFixed(2)
  }
  // Aquí: string (number eliminado por el return)
  
  return valor.toUpperCase()
}

El análisis funciona con asignaciones que cambian el tipo en función del valor asignado:

let resultado: string | number

resultado = "hola"
// TypeScript sabe que resultado es string
console.log(resultado.toUpperCase())

resultado = 42
// TypeScript sabe que resultado es number
console.log(resultado.toFixed(2))

También funciona con estructuras más complejas como bucles y operadores lógicos:

function buscarPrimero(items: (string | null)[]): string {
  for (const item of items) {
    if (item !== null) {
      return item // TypeScript sabe que item es string
    }
  }
  throw new Error("Ningun elemento encontrado")
}

function valorODefault(valor: string | undefined, defecto: string): string {
  // El operador || no estrecha tipos, pero ?? si elimina null/undefined
  return valor ?? defecto
}

El análisis de control de flujo reconoce funciones que nunca retornan (tipo never) y las usa para estrechar en las ramas restantes:

function lanzar(mensaje: string): never {
  throw new Error(mensaje)
}

function obtenerValor(datos: { valor?: string }): string {
  if (datos.valor !== undefined) {
    return datos.valor
  }
  lanzar("Valor requerido")
  // TypeScript sabe que está linea es inalcanzable
}

Un ejemplo completo que combina múltiples técnicas de narrowing:

type Entrada =
  | { tipo: "texto"; contenido: string }
  | { tipo: "número"; contenido: number }
  | { tipo: "lista"; contenido: string[] }

function validarEntrada(entrada: unknown): Entrada {
  if (typeof entrada !== "object" || entrada === null) {
    throw new Error("Entrada debe ser un objeto")
  }
  
  if (!("tipo" in entrada) || !("contenido" in entrada)) {
    throw new Error("Faltan campos requeridos")
  }
  
  const obj = entrada as { tipo: unknown; contenido: unknown }
  
  if (obj.tipo === "texto" && typeof obj.contenido === "string") {
    return { tipo: "texto", contenido: obj.contenido }
  }
  
  if (obj.tipo === "número" && typeof obj.contenido === "number") {
    return { tipo: "número", contenido: obj.contenido }
  }
  
  if (obj.tipo === "lista" && Array.isArray(obj.contenido)) {
    return { tipo: "lista", contenido: obj.contenido as string[] }
  }
  
  throw new Error(`Tipo no soportado: ${obj.tipo}`)
}

function procesarEntrada(entrada: Entrada): string {
  switch (entrada.tipo) {
    case "texto":
      return `Texto: ${entrada.contenido.toUpperCase()}`
    case "número":
      return `Número: ${entrada.contenido.toFixed(2)}`
    case "lista":
      return `Lista: ${entrada.contenido.join(", ")}`
  }
}

const datos = validarEntrada({ tipo: "lista", contenido: ["a", "b", "c"] })
console.log(procesarEntrada(datos)) // "Lista: a, b, c"

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en TypeScript

Documentación oficial de TypeScript
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Dominar typeof guards para tipos primitivos. Aplicar instanceof guards para jerarquías de clases. Usar el operador in para comprobar existencia de propiedades. Comprender equality narrowing y truthiness narrowing. Crear type predicates personalizados con la palabra clave is. Implementar assertion functions con asserts. Entender el análisis de control de flujo de TypeScript.