Tipos de unión e intersección

Intermedio
TypeScript
TypeScript
Actualizado: 04/05/2026

Diagrama: tutorial-typescript-union-interseccion

Tipos de unión con el operador |

Los tipos de unión permiten expresar que un valor puede pertenecer a uno de varios tipos posibles. El operador | combina dos o más tipos, creando un tipo que acepta cualquier valor válido de sus miembros:

let identificador: string | number

identificador = "USR-001"
identificador = 42
// identificador = true // Error: Type 'boolean' is not assignable to type 'string | number'

Cuando una variable tiene un tipo de unión, TypeScript solo permite acceder a las propiedades y métodos que son comunes a todos los miembros de la unión:

function mostrarLongitud(valor: string | string[]): number {
  // .length existe tanto en string como en string[]
  return valor.length
}

console.log(mostrarLongitud("hola")) // 4
console.log(mostrarLongitud(["a", "b", "c"])) // 3

Si intentamos acceder a un método que solo existe en uno de los tipos, TypeScript emite un error hasta que realicemos una comprobación:

function procesar(valor: string | number): string {
  // valor.toUpperCase() // Error: Property 'toUpperCase' does not exist on type 'number'
  
  if (typeof valor === "string") {
    return valor.toUpperCase()
  }
  return valor.toFixed(2)
}

console.log(procesar("typescript")) // "TYPESCRIPT"
console.log(procesar(3.14159)) // "3.14"

Uniones con null y undefined

Un uso frecuente de las uniones es representar valores que pueden ser nulos o indefinidos:

function buscarUsuario(id: number): string | null {
  if (id === 1) return "Ana"
  if (id === 2) return "Carlos"
  return null
}

const nombre = buscarUsuario(3)
if (nombre !== null) {
  console.log(nombre.toUpperCase()) // TypeScript sabe que es string
}

El operador de coalescencia nula ?? simplifica el manejo de valores null o undefined:

function obtenerConfig(clave: string): string | undefined {
  const configs: Record<string, string> = { tema: "oscuro", idioma: "es" }
  return configs[clave]
}

const tema = obtenerConfig("tema") ?? "claro"
const fuente = obtenerConfig("fuente") ?? "sans-serif"
console.log(tema) // "oscuro"
console.log(fuente) // "sans-serif"

Uniones en parámetros y retornos de funciones

Las uniones permiten crear funciones flexibles que manejan varios tipos de entrada:

function formatearFecha(fecha: Date | string | number): string {
  if (typeof fecha === "string") {
    return new Date(fecha).toLocaleDateString("es-ES")
  }
  if (typeof fecha === "number") {
    return new Date(fecha).toLocaleDateString("es-ES")
  }
  return fecha.toLocaleDateString("es-ES")
}

console.log(formatearFecha(new Date())) 
console.log(formatearFecha("2025-06-15"))
console.log(formatearFecha(1718409600000))

También resultan útiles para representar distintos formatos de entrada en APIs:

type Criterio = string | { campo: string; valor: string } | string[]

function buscar(criterio: Criterio): string {
  if (typeof criterio === "string") {
    return `Busqueda por texto: ${criterio}`
  }
  if (Array.isArray(criterio)) {
    return `Busqueda por etiquetas: ${criterio.join(", ")}`
  }
  return `Busqueda por campo ${criterio.campo} = ${criterio.valor}`
}

console.log(buscar("typescript"))
console.log(buscar(["frontend", "web"]))
console.log(buscar({ campo: "autor", valor: "Ana" }))

Tipos de intersección con el operador &

Los tipos de intersección combinan múltiples tipos en uno solo que posee todas las propiedades de cada tipo constituyente. El operador & crea un tipo que es la unión de las propiedades de ambos lados:

type ConNombre = {
  nombre: string
}

type ConEdad = {
  edad: number
}

type Persona = ConNombre & ConEdad

const persona: Persona = {
  nombre: "Elena",
  edad: 30
}

console.log(`${persona.nombre}, ${persona.edad} anios`)

En una unión (A | B) el valor es A o B. En una intersección (A & B) el valor es A y B simultáneamente. La unión amplía las posibilidades; la intersección amplía las propiedades.

Composición de tipos con intersecciones

Las intersecciones permiten componer tipos complejos a partir de piezas más pequeñas y reutilizables:

type Timestamps = {
  creadoEn: Date
  actualizadoEn: Date
}

type SoftDelete = {
  eliminado: boolean
  eliminadoEn: Date | null
}

type DatosProducto = {
  id: string
  nombre: string
  precio: number
}

type Producto = DatosProducto & Timestamps & SoftDelete

const producto: Producto = {
  id: "PROD-001",
  nombre: "Laptop",
  precio: 1299,
  creadoEn: new Date("2025-01-01"),
  actualizadoEn: new Date("2025-03-15"),
  eliminado: false,
  eliminadoEn: null
}

Esta estrategia es especialmente útil para añadir metadatos transversales a múltiples entidades:

type Auditable = {
  creadoPor: string
  modificadoPor: string
}

type Versionable = {
  version: number
  historial: string[]
}

type DatosDocumento = {
  titulo: string
  contenido: string
}

type Documento = DatosDocumento & Auditable & Versionable

function crearDocumento(titulo: string, contenido: string, autor: string): Documento {
  return {
    titulo,
    contenido,
    creadoPor: autor,
    modificadoPor: autor,
    version: 1,
    historial: [`v1: Creado por ${autor}`]
  }
}

const doc = crearDocumento("Manual", "Contenido del manual", "Ana")
console.log(doc.historial) // ["v1: Creado por Ana"]

Intersecciones con interfaces

Las intersecciones también funcionan con interfaces, permitiendo combinar contratos existentes:

interface Serializable {
  toJSON(): string
}

interface Clonable {
  clonar(): this
}

interface Imprimible {
  imprimir(): void
}

type ObjetoCompleto = Serializable & Clonable & Imprimible

class Nota implements ObjetoCompleto {
  constructor(private texto: string) {}

  toJSON(): string {
    return JSON.stringify({ texto: this.texto })
  }

  clonar(): this {
    return new Nota(this.texto) as this
  }

  imprimir(): void {
    console.log(`Nota: ${this.texto}`)
  }
}

Conflictos en intersecciones

Cuando dos tipos tienen una propiedad con el mismo nombre pero tipos incompatibles, la intersección produce el tipo never para esa propiedad, lo que impide crear valores válidos:

type A = { valor: string }
type B = { valor: number }
type Conflicto = A & B

// const imposible: Conflicto = { valor: ??? }
// No hay ningun valor que sea string y number a la vez
// El tipo de 'valor' es 'never'

Si los tipos son compatibles, la intersección los combina correctamente:

type Base = { id: string; nombre: string }
type Extension = { nombre: string; email: string }
type Combinado = Base & Extension

const combinado: Combinado = {
  id: "1",
  nombre: "Ana", // nombre es string en ambos, no hay conflicto
  email: "ana@ejemplo.com"
}

Combinación con type aliases

Los type aliases proporcionan nombres reutilizables para uniones e intersecciones complejas. Esto mejora la legibilidad y facilita el mantenimiento:

type Id = string | number
type Estado = "activo" | "inactivo" | "pendiente"
type Respuesta<T> = { datos: T; exito: true } | { error: string; exito: false }

function obtenerUsuario(id: Id): Respuesta<{ nombre: string; estado: Estado }> {
  if (typeof id === "number" && id > 0) {
    return {
      datos: { nombre: "Usuario " + id, estado: "activo" },
      exito: true
    }
  }
  return { error: "ID no válido", exito: false }
}

const resultado = obtenerUsuario(5)
if (resultado.exito) {
  console.log(resultado.datos.nombre)
} else {
  console.log(resultado.error)
}

Los type aliases pueden combinar uniones e intersecciones para modelar dominios complejos:

type MetodoPago = "tarjeta" | "transferencia" | "efectivo"

type DireccionEnvio = {
  calle: string
  ciudad: string
  codigoPostal: string
}

type InfoFacturacion = {
  nif: string
  razonSocial: string
}

type OpcionesEnvio = {
  direccion: DireccionEnvio
  urgente: boolean
}

type PedidoOnline = {
  items: { nombre: string; cantidad: number; precio: number }[]
  metodoPago: MetodoPago
} & OpcionesEnvio & InfoFacturacion

type PedidoTienda = {
  items: { nombre: string; cantidad: number; precio: number }[]
  metodoPago: MetodoPago
  tienda: string
}

type Pedido = PedidoOnline | PedidoTienda

function calcularTotal(pedido: Pedido): number {
  return pedido.items.reduce((total, item) => total + item.cantidad * item.precio, 0)
}

function esOnline(pedido: Pedido): pedido is PedidoOnline {
  return "direccion" in pedido
}

function procesarPedido(pedido: Pedido): string {
  const total = calcularTotal(pedido)
  if (esOnline(pedido)) {
    const envio = pedido.urgente ? " (URGENTE)" : ""
    return `Online${envio} a ${pedido.direccion.ciudad}: ${total}e`
  }
  return `Tienda ${pedido.tienda}: ${total}e`
}

Uniones de tipos objeto

Las uniones de tipos objeto son habituales cuando distintos estados comparten parte de su estructura:

type EventoSistema =
  | { tipo: "inicio"; timestamp: Date }
  | { tipo: "error"; timestamp: Date; mensaje: string; codigo: number }
  | { tipo: "fin"; timestamp: Date; duracion: number }

function registrarEvento(evento: EventoSistema): string {
  const fecha = evento.timestamp.toISOString()
  
  switch (evento.tipo) {
    case "inicio":
      return `[${fecha}] Sistema iniciado`
    case "error":
      return `[${fecha}] Error ${evento.codigo}: ${evento.mensaje}`
    case "fin":
      return `[${fecha}] Finalizado en ${evento.duracion}ms`
  }
}

const eventos: EventoSistema[] = [
  { tipo: "inicio", timestamp: new Date() },
  { tipo: "error", timestamp: new Date(), mensaje: "Timeout", codigo: 408 },
  { tipo: "fin", timestamp: new Date(), duracion: 1500 }
]

for (const evento of eventos) {
  console.log(registrarEvento(evento))
}

Un patrón útil es extraer partes comunes con intersecciones y luego unir las variantes:

type MetadatosBase = {
  id: string
  versión: number
  creadoEn: Date
}

type ConfiguracionServidor = MetadatosBase & {
  tipo: "servidor"
  host: string
  puerto: number
  ssl: boolean
}

type ConfiguracionBBDD = MetadatosBase & {
  tipo: "bbdd"
  conexion: string
  maxConexiones: number
}

type ConfiguracionCache = MetadatosBase & {
  tipo: "cache"
  ttl: number
  maxEntradas: number
}

type Configuracion = ConfiguracionServidor | ConfiguracionBBDD | ConfiguracionCache

function describir(config: Configuracion): string {
  const base = `[${config.id} v${config.version}]`
  
  switch (config.tipo) {
    case "servidor":
      return `${base} Servidor: ${config.host}:${config.puerto} SSL=${config.ssl}`
    case "bbdd":
      return `${base} BBDD: ${config.conexion} (max: ${config.maxConexiones})`
    case "cache":
      return `${base} Cache: TTL=${config.ttl}s, max=${config.maxEntradas}`
  }
}

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

Comprender la sintaxis y el comportamiento de los tipos de union con el operador |. Dominar los tipos de intersección con el operador & para combinar estructuras. Aplicar narrowing básico para trabajar con uniones de forma segura. Combinar uniones e intersecciones con type aliases para modelar dominios complejos.