🇺🇸 EN
TypeScript
·6 min read·↗·🇺🇸 EN

Cómo combinar tipos e interfaces con & y | en TypeScript

Portada principal del artículo: Cómo combinar tipos e interfaces con & y | en TypeScript

Al modelar los datos de una aplicación en TypeScript, rápidamente llegas a un punto donde los tipos básicos no son suficientes. Necesitas componer tipos: combinar datos de usuario con permisos, manejar respuestas de API que devuelven datos exitosos o un payload de error, o extender interfaces de terceros existentes.

TypeScript nos da tres herramientas principales para lograr esto:

  1. Tipos Union (|)
  2. Tipos Intersection (&)
  3. Herencia de interfaces (extends)

En esta guía, vamos a ver cómo funciona cada operador, cuándo usarlos en aplicaciones del mundo real y cómo evitar errores comunes al componer tipos.

Empecemos revisando nuestros modelos de dominio base.


1. Las interfaces base 🏀

Imagina que estamos construyendo un panel de gestión deportiva. Tenemos dos interfaces separadas que describen aspectos diferentes del dominio:

interface Player {
  id: number;
  name: string;
}
 
interface PlayerStats {
  points: number;
  position: string;
}
  • Player maneja la información de identidad.
  • PlayerStats maneja las métricas de rendimiento en el partido.

¿Cómo las combinamos cuando una funcionalidad necesita ambas estructuras o cualquiera de ellas? Veamos primero los tipos Union.


2. Tipos Union (|): manejar múltiples posibilidades 🔀

Un tipo Union (usando el operador |) le dice a TypeScript: "Este valor puede ser de Tipo A O de Tipo B."

export type PlayerOrStats = Player | PlayerStats;

Una variable de tipo PlayerOrStats puede ser:

  1. Un objeto que cumple con Player
  2. Un objeto que cumple con PlayerStats
  3. Un objeto que cumple con ambos

Ejemplos prácticos

// ✅ 1. Válido: solo propiedades de Player
const playerOnly: PlayerOrStats = {
  id: 1,
  name: "Diana Taurasi"
};
 
// ✅ 2. Válido: solo propiedades de PlayerStats
const statsOnly: PlayerOrStats = {
  points: 24,
  position: "Guard"
};
 
// ✅ 3. Válido: ambos grupos de propiedades
const fullPlayer: PlayerOrStats = {
  id: 2,
  name: "Breanna Stewart",
  points: 30,
  position: "Forward"
};

Acceso a propiedades en tipos Union

Al acceder a propiedades en un tipo unión, TypeScript solo permite acceder a propiedades que sean comunes a todos los miembros de la unión, a menos que reduzcas el tipo:

function printPlayerInfo(data: PlayerOrStats) {
  // ❌ Error: Property 'name' does not exist on type 'PlayerStats'
  // console.log(data.name);
 
  // ✅ Solución: reducir el tipo con el operador 'in'
  if ("name" in data) {
    console.log(`Player Name: ${data.name}`);
  }
}

Si quieres aprender todas las formas de verificar y reducir tipos unión de forma segura en tiempo de ejecución, revisa mi artículo sobre cómo verificar tipos en TypeScript.

Ahora que entendemos las uniones, veamos qué pasa cuando necesitamos un objeto que exija estrictamente todas las propiedades.


3. Tipos Intersection (&): combinar múltiples tipos 🔗

Un tipo Intersection (usando el operador &) combina múltiples tipos en uno solo. Le indica a TypeScript: "Este valor DEBE cumplir con Tipo A Y Tipo B."

export type CompletePlayer = Player & PlayerStats;

Con CompletePlayer, cualquier objeto asignado debe incluir todas las propiedades tanto de Player como de PlayerStats:

// ❌ Error: faltan las propiedades 'points' y 'position'
const incompletePlayer: CompletePlayer = {
  id: 1,
  name: "A'ja Wilson"
};
 
// ✅ Válido: contiene todas las propiedades requeridas de ambas interfaces
const validPlayer: CompletePlayer = {
  id: 1,
  name: "A'ja Wilson",
  points: 28,
  position: "Center"
};

Problema común en intersecciones: tipos de propiedades incompatibles (never)

Ten cuidado al intersectar tipos que declaran el mismo nombre de propiedad con tipos primitivos incompatibles:

interface ServerResponseA {
  status: string;
}
 
interface ServerResponseB {
  status: number;
}
 
// ⚠️ 'status' se convierte en (string & number), lo cual resulta en 'never'
type MergedResponse = ServerResponseA & ServerResponseB;

Como una propiedad no puede ser string y number a la vez, el campo status se convierte en never, haciendo imposible instanciar MergedResponse sin recurrir a una aserción de tipo.

Ahora vamos a comparar los tipos intersección con la herencia de interfaces.


4. interface extends vs Intersección (&) 🏛️

Las interfaces de TypeScript ofrecen una sintaxis alternativa para la composición: extends.

interface ExtendedPlayer extends Player {
  points: number;
  position: string;
}

Tanto CompletePlayer (Player & PlayerStats) como ExtendedPlayer requieren la misma estructura final. Así es como se comparan en la práctica:

CaracterísticaHerencia de interfaces (extends)Tipos Intersection (&)
Caso de uso principalContratos de objetos jerárquicos y entidades de dominioComposición puntual, tipos de utilidad y uniones
Flexibilidad de tiposSolo puede extender formas de objetos y tipos conocidos estáticamentePuede componer cualquier tipo (primitivos, uniones, genéricos)
Rendimiento del compiladorAlmacenamiento en caché más rápido por el compilador de TypeScriptSe calcula de forma dinámica en cada evaluación
Detección de conflictosEmite un error de compilación si los tipos de las propiedades no coincidenFusiona primitivos en conflicto en never
Declaration MergingSoportado (las interfaces con el mismo nombre se combinan solas)No soportado en alias de tipos

Cuándo usar extends:

  • Al diseñar tus entidades de dominio principales y jerarquías de objetos.
  • Cuando quieras que el compilador de TypeScript te avise de inmediato si un subtipo intenta sobrescribir una propiedad del padre con un tipo incompatible.

Cuándo usar & (Intersección):

  • Al combinar un alias de tipo con una interfaz.
  • Al crear tipos de utilidad o combinar restricciones genéricas en funciones.
  • Al combinar miembros de una unión de forma dinámica.

A continuación, veamos uno de los patrones más útiles en el mundo real: las uniones discriminadas.


5. Patrón en el mundo real: Uniones discriminadas 🛡️

El patrón de arquitectura más común que combina uniones e intersecciones en producción son las uniones discriminadas (Discriminated Unions) para estados asíncronos de API:

type LoadingState = { status: "loading" };
type SuccessState<T> = { status: "success"; data: T };
type ErrorState = { status: "error"; message: string };
 
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
 
function handleState(state: AsyncState<Player[]>) {
  switch (state.status) {
    case "loading":
      return "Loading players...";
    case "success":
      return `Loaded ${state.data.length} players.`;
    case "error":
      return `Error: ${state.message}`;
  }
}

Como cada tipo tiene una propiedad literal única (status), TypeScript reduce automáticamente el tipo del objeto dentro de cada bloque case con total seguridad de tipos y autocompletado.


Resumen 🛠️

Aquí tienes un resumen rápido para guiar tus decisiones del día a día al escribir código:

  1. Union (|): Úsalo cuando un valor pueda ser uno de varios tipos posibles (por ejemplo, string | number o variantes de peticiones a una API).
  2. Intersection (&): Úsalo cuando necesites fusionar tipos existentes, tipos de utilidad o genéricos en un solo tipo compuesto.
  3. interface extends: Úsalo para jerarquías estándar de orientación a objetos y entidades claras de tu dominio.
  4. Uniones discriminadas: Combina etiquetas literales con uniones para crear máquinas de estado robustas y gestionar respuestas de API.

Si quieres conocer más técnicas prácticas de TypeScript, revisa mis artículos sobre cómo verificar tipos en TypeScript, 3 formas de transformar tipos en TypeScript y por qué deberías evitar el uso de 'any' en TypeScript.

Parte de la serie TypeScript

Mastering the language of the web. From basic types to advanced generics and patterns.

Ver serie completa

Preguntas Frecuentes

¿Cuál es la diferencia entre un tipo Union (|) y un tipo Intersection (&) en TypeScript?

Un tipo Union (|) significa que un valor puede cumplir con TypeA o TypeB (teniendo las propiedades de al menos uno de los miembros). Un tipo Intersection (&) combina tipos, exigiendo que un valor cumpla con TypeA y TypeB al mismo tiempo.

¿Cuándo debo usar interface extends en lugar de un tipo Intersection (&)?

Usa interface extends cuando definas contratos de objetos jerárquicos dentro de tu modelo de dominio (ofrece mejor rendimiento del compilador y mensajes de error claros). Usa tipos Intersection (&) al combinar tipos existentes, miembros de una unión o tipos genéricos puntuales.

¿Qué sucede si dos interfaces tienen tipos de propiedades en conflicto en una intersección?

Si dos interfaces comparten una propiedad con tipos primitivos incompatibles (por ejemplo id: string & id: number), TypeScript evalúa el tipo de la propiedad como never, haciendo imposible instanciar el objeto.

¿Puedo combinar tipos unión con tipos intersección en TypeScript?

Sí. Intersectar un tipo con una unión distribuye la intersección entre cada miembro de la unión (por ejemplo, Base & (VariantA | VariantB) da como resultado (Base & VariantA) | (Base & VariantB)), un patrón común para metadatos compartidos de entidades.

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.