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:
- Tipos Union (
|) - Tipos Intersection (
&) - 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;
}Playermaneja la información de identidad.PlayerStatsmaneja 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:
- Un objeto que cumple con
Player - Un objeto que cumple con
PlayerStats - 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ística | Herencia de interfaces (extends) | Tipos Intersection (&) |
|---|---|---|
| Caso de uso principal | Contratos de objetos jerárquicos y entidades de dominio | Composición puntual, tipos de utilidad y uniones |
| Flexibilidad de tipos | Solo puede extender formas de objetos y tipos conocidos estáticamente | Puede componer cualquier tipo (primitivos, uniones, genéricos) |
| Rendimiento del compilador | Almacenamiento en caché más rápido por el compilador de TypeScript | Se calcula de forma dinámica en cada evaluación |
| Detección de conflictos | Emite un error de compilación si los tipos de las propiedades no coinciden | Fusiona primitivos en conflicto en never |
| Declaration Merging | Soportado (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:
- Union (
|): Úsalo cuando un valor pueda ser uno de varios tipos posibles (por ejemplo,string | numbero variantes de peticiones a una API). - Intersection (
&): Úsalo cuando necesites fusionar tipos existentes, tipos de utilidad o genéricos en un solo tipo compuesto. interface extends: Úsalo para jerarquías estándar de orientación a objetos y entidades claras de tu dominio.- 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.
Mastering the language of the web. From basic types to advanced generics and patterns.
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
Cómo comprobar tipos en TypeScript (Guía completa)
Aprende cómo comprobar y validar tipos en TypeScript en tiempo de compilación y en tiempo de ejecución usando typeof, instanceof, el operador in, uniones discriminadas, type guards personalizados y Zod o Valibot.
3 formas de transformar tipos en TypeScript
Cuando usamos tipos o interfaces en TypeScript, el compilador exige que el objeto coincida con ellos para evitar errores en tiempo de ejecución por propiedades faltantes. A veces queremos la flexibilidad de crear un objeto sin romper el contrato con la interfaz.
Por qué deberías evitar usar 'any' en TypeScript
Por qué usar any en TypeScript genera errores ocultos en tiempo de ejecución, y cómo reemplazarlo con unknown, guardas de tipo personalizadas, tipos de utilidad y el operador satisfies.
Cómo detectar y corregir dependencias circulares en TypeScript
Aprende por qué las dependencias circulares rompen aplicaciones TypeScript en tiempo de ejecución, cómo detectarlas usando Madge y ESLint, y cómo solucionarlas usando importaciones de solo tipos y patrones limpios de arquitectura.
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.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.