learning-front

Nivel 5 · TypeScript: JavaScript con red de seguridad

Uniones e intersecciones

Componer tipos: un valor que puede ser varias cosas o que es varias a la vez, y las uniones discriminadas que un campo común vuelve seguras de distinguir.

Hasta ahora has tipado valores sueltos (capítulo 1) y objetos con forma fija (capítulo 2). Pero los datos de un sistema real son más ricos: un campo puede aceptar varios tipos distintos, un conjunto de valores puede ser cerrado, un objeto puede ser la combinación de dos formas, y un array puede contener elementos que son “una cosa o la otra”.

Para eso existen las uniones y las intersecciones: dos operadores con los que componer tipos más expresivos a partir de los que ya tienes.

Unión A | B: un valor que puede ser de varios tipos#

El operador | crea una unión: el valor puede ser de cualquiera de los tipos indicados. Es la forma de decirle a TypeScript “aquí puede llegar esto o lo otro”.

typescript
// El id de un héroe puede llegar como número desde la BD
// o como texto desde una URL. La unión modela ese contrato real.
type IdHeroe = number | string;

Con este tipo, TypeScript acepta 1 y acepta "tracer-29", pero rechaza true o null. Tienes flexibilidad donde la necesitas y protección donde no.

Tipos literales: un valor concreto como tipo#

Un tipo literal usa un valor concreto —en lugar de la categoría string o number— como tipo. La unión de literales forma un conjunto cerrado:

typescript
// Solo estos tres valores son válidos. Cualquier otro es un error de compilación.
type Rol = "Daño" | "Tanque" | "Apoyo";

¿Por qué importa? Porque en el juego no existe el rol “Support” ni “DPS”. Si alguien escribe uno de esos valores, el error sale en compilación, no cuando el dato llega a producción. El tipo documenta el dominio y lo protege al mismo tiempo.

typescript
// Correcto: "Apoyo" pertenece al conjunto.
const rolActual: Rol = "Apoyo";

// Error: "Support" no es ninguno de los tres literales.
// const rolMal: Rol = "Support";

Intersección A & B: un valor que es ambas formas a la vez#

Donde | dice “una cosa o la otra”, & dice “una cosa y la otra”: el resultado tiene todos los campos de ambos tipos. Se usa para combinar formas que modelan responsabilidades distintas.

typescript
// La identidad del héroe: quién es.
interface HeroeBase {
  readonly id: number;
  readonly nombre: string;
  readonly rol: string;
}

// Las estadísticas: cómo juega.
interface Stats {
  partidas: number;
  victorias: number;
}

// Un héroe completo: tiene que cumplir los dos contratos.
type HeroeConStats = HeroeBase & Stats;

Un objeto de tipo HeroeConStats necesita todos los campos de HeroeBase y todos los de Stats. Si falta uno solo, TypeScript protesta.

Uniones discriminadas: distinguir variantes con seguridad#

Una unión discriminada (también llamada tagged union) es una unión de objetos donde cada miembro tiene un campo con un tipo literal único. Ese campo —el discriminante— le permite a TypeScript saber con exactitud de qué variante se trata.

typescript
// Cada interface declara su "tipo" con un literal diferente.
interface EventoFichaje {
  // el discriminante: solo este miembro lleva el literal "fichaje"
  tipo: "fichaje";
  heroe: string;
  fechaAlta: string;
}

interface EventoBaja {
  // el discriminante: solo este miembro lleva el literal "baja"
  tipo: "baja";
  idHeroe: number;
  motivo: string;
}

type EventoEquipo = EventoFichaje | EventoBaja;

La clave: al comparar evento.tipo, TypeScript estrecha el tipo automáticamente. Dentro del bloque if (evento.tipo === "fichaje"), ya sabe que evento es un EventoFichaje y que existen heroe y fechaAlta. Intentar acceder a idHeroe ahí es un error de compilación.

typescript
function procesarEvento(evento: EventoEquipo): void {
  if (evento.tipo === "fichaje") {
    // Aquí TypeScript sabe que es EventoFichaje.
    console.log(evento.heroe + "" + evento.fechaAlta);
  } else {
    // Aquí TypeScript sabe que es EventoBaja.
    console.log("id " + evento.idHeroe + "" + evento.motivo);
  }
}

Al comparar el campo discriminante con ===, TypeScript descarta dentro del if las variantes que no encajan y deja solo la que coincide; eso es estrechar el tipo (narrowing), y se estudia a fondo en el capítulo siguiente.

Nota sobre lo que no hace falta todavía: estás comparando el campo discriminante directamente con ===. Eso es todo lo que se necesita aquí. El toolkit completo de narrowingtypeof, in, instanceof, funciones predicado— es el capítulo siguiente.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué significa `type Id = number | string`?

Tu turno#

El registro de la temporada tiene fichajes y bajas, pero sin tipos: TypeScript no conoce la forma de cada evento. Define la unión, las interfaces con sus campos literales y el resto de anotaciones hasta que el panel quede vacío. Cuando lo tengas, despliega las soluciones y fíjate en el salto de un nivel al siguiente.

Ejercicio · en esta página

Modela los eventos del Team Builder

El registro de la temporada contiene fichajes y bajas, pero el fichero no tiene tipos: TypeScript no sabe qué forma tiene cada evento. Define las interfaces, la unión y los tipos literales necesarios hasta que el panel "Problemas" quede vacío y la consola muestre todos los eventos.

Paso 1: Que funcione

  • Defines EventoFichaje y EventoBaja como interfaces separadas con su campo "tipo" literal.
  • Unes ambas en EventoEquipo y anotas el array y el parámetro de procesarEvento.
  • El panel "Problemas" queda vacío y la consola muestra los cuatro eventos.
Ver soluciones
// Solución OK: unión básica que funciona y elimina todos los errores.
// EventoEquipo es la unión de dos interfaces; el campo "tipo" permite
// distinguirlas con un if. Suficiente para que el panel quede vacío.

interface HeroeBase {
  readonly id: number;
  readonly nombre: string;
  readonly rol: string;
}

interface Heroe extends HeroeBase {
  partidas: number;
  victorias: number;
}

// Interfaz para el evento de fichaje.
interface EventoFichaje {
  tipo: "fichaje";
  heroe: Heroe;
  fechaAlta: string;
}

// Interfaz para el evento de baja.
interface EventoBaja {
  tipo: "baja";
  idHeroe: number;
  motivo: string;
}

// La unión: un EventoEquipo es un fichaje O una baja.
type EventoEquipo = EventoFichaje | EventoBaja;

const equipo: Heroe[] = [
  { id: 1, nombre: "Tracer",    rol: "Daño",   partidas: 120, victorias: 78  },
  { id: 2, nombre: "Mercy",     rol: "Apoyo",  partidas: 200, victorias: 155 },
  { id: 3, nombre: "Reinhardt", rol: "Tanque", partidas: 95,  victorias: 61  },
];

// El array queda anotado: TypeScript sabe que cada elemento es un EventoEquipo.
const registro: EventoEquipo[] = [
  { tipo: "fichaje", heroe: equipo[0], fechaAlta: "2026-01-10" },
  { tipo: "fichaje", heroe: equipo[1], fechaAlta: "2026-01-12" },
  { tipo: "baja",    idHeroe: 99,      motivo: "inactividad"   },
  { tipo: "fichaje", heroe: equipo[2], fechaAlta: "2026-02-01" },
];

// Comparar el discriminante "tipo" nos dirige al miembro correcto de la unión.
function procesarEvento(evento: EventoEquipo): void {
  if (evento.tipo === "fichaje") {
    // Aquí TypeScript sabe que evento es EventoFichaje.
    console.log("[FICHAJE] " + evento.heroe.nombre + "" + evento.fechaAlta);
  } else {
    // Aquí TypeScript sabe que evento es EventoBaja.
    console.log("[BAJA] id " + evento.idHeroe + "" + evento.motivo);
  }
}

registro.forEach(procesarEvento);

Por qué este nivel

  • Dos interfaces con su campo "tipo" literal y la unión EventoEquipo: el mínimo para que TypeScript distinga los dos casos y el panel quede vacío.
  • La comparación if (evento.tipo === "fichaje") es suficiente para que TypeScript sepa en qué variante está y dé acceso a sus campos específicos.
  • Su límite: no hay tipos literales para los roles (cualquier string pasaría), no hay una tercera variante y los objetos del registro son mutables.