Uniones, intersecciones y alias de tipo en TypeScript
En esta lección
Hay valores que pueden ser de más de un tipo: un código de producto que a veces es número y a veces texto, un usuario que puede no haber iniciado sesión (null), un pago que puede ser con tarjeta o con Bizum. En esta lección aprenderás a describirlos con tipos unión (A | B), a combinar tipos con intersecciones (A & B) y a poner nombre a cualquier tipo con type. Al final sabrás cuándo usar type y cuándo interface.
Tipos unión
Un tipo unión dice que un valor puede ser de uno u otro tipo. Se escribe separando los tipos con una barra vertical |, que se lee “o”:
let codigoPostal: string | number;
codigoPostal = "39008";
codigoPostal = 39008;
codigoPostal = true;
// Error: Type 'boolean' is not assignable to type 'string | number'.
Puedes unir tantos tipos como quieras, y se usan igual en variables, parámetros, propiedades y arrays:
function mostrarTalla(talla: number | string): string {
return `Talla: ${talla}`;
}
console.log(mostrarTalla(42));
console.log(mostrarTalla("XL"));
// Talla: 42
// Talla: XL
Con una unión solo puedes usar lo común
Aquí está la clave de las uniones. Si un valor es string | number, TypeScript solo te deja hacer con él lo que se puede hacer con los dos tipos:
function formatear(valor: string | number): string {
console.log(valor.toString()); // bien: string y number tienen toString
return valor.toUpperCase();
}
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
Tiene sentido: si te pasan un número, toUpperCase no existe y el programa fallaría. Para usar algo propio de uno de los tipos, primero compruebas cuál es. Dentro de cada rama, TypeScript ya sabe el tipo exacto:
function formatear(valor: string | number): string {
if (typeof valor === "string") {
return valor.toUpperCase(); // aquí valor es string
}
return valor.toFixed(2); // aquí solo puede ser number
}
console.log(formatear("rebajas"));
console.log(formatear(7.5));
// REBAJAS
// 7.50
Esta técnica se llama estrechar el tipo (narrowing): a partir de una comprobación, TypeScript reduce la unión a una de sus partes. Tiene muchas variantes (typeof, in, instanceof, comparaciones…) y la verás a fondo en Estrechar tipos. Por ahora te basta con typeof.
Nota: una unión no “convierte” nada.
string | numberno significa que el valor sea las dos cosas a la vez, sino que en cada momento es una de ellas y tú tienes que averiguar cuál antes de usar lo específico.
Valores que pueden faltar: unión con null o undefined
El uso más frecuente de las uniones es indicar que un valor puede no existir. Con strict activado, null y undefined no caben en un string normal, así que hay que decirlo:
let usuarioConectado: string | null = null;
function saludo(): string {
if (usuarioConectado === null) {
return "Hola, invitado";
}
return `Hola, ${usuarioConectado}`;
}
console.log(saludo());
usuarioConectado = "Marta";
console.log(saludo());
// Hola, invitado
// Hola, Marta
Los operadores ?. (encadenamiento opcional) y ?? (valor por defecto) de JavaScript encajan perfectamente con estas uniones:
function iniciales(nombre: string | undefined): string {
return nombre?.slice(0, 2).toUpperCase() ?? "--";
}
console.log(iniciales("Santander"));
console.log(iniciales(undefined));
// SA
// --
Consejo: usa
nullpara “no hay valor a propósito” (el usuario no ha iniciado sesión) y dejaundefinedpara lo que simplemente no se ha dado, como un parámetro o una propiedad opcional. No es obligatorio, pero hace el código más predecible.
Uniones de literales
En Tipos básicos viste los tipos literales: tipos que admiten un único valor. Unidos con | describen un conjunto cerrado de opciones, y el editor te las sugiere al escribir:
let estado: "pendiente" | "enviado" | "entregado" = "pendiente";
estado = "enviado";
estado = "perdido";
// Error: Type '"perdido"' is not assignable to type '"pendiente" | "enviado" | "entregado"'.
También funcionan con números (1 | 2 | 3 | 4 | 5 para una valoración) y se pueden mezclar con otros tipos (number | "auto" para un ancho que es un número o la palabra "auto"). En Enums y literales comparamos estas uniones con los enum.
Alias de tipo con type
Escribir "pendiente" | "enviado" | "entregado" cada vez que lo necesitas es largo y fácil de equivocar. Un alias de tipo le da un nombre:
type EstadoPedido = "pendiente" | "enviado" | "entregado";
type Id = string | number;
function cambiarEstado(id: Id, nuevo: EstadoPedido): string {
return `Pedido ${id} → ${nuevo}`;
}
console.log(cambiarEstado(1043, "enviado"));
console.log(cambiarEstado("P-2001", "entregado"));
// Pedido 1043 → enviado
// Pedido P-2001 → entregado
Se escribe type, el nombre en PascalCase, un = y el tipo. A diferencia de interface, un alias puede nombrar cualquier tipo:
type Precio = number; // un primitivo
type Coordenada = [lat: number, lon: number]; // una tupla
type Etiquetas = string[]; // un array
type Validador = (texto: string) => boolean; // una función
type Usuario = { nombre: string; email: string | null }; // un objeto
const esEmail: Validador = (texto) => texto.includes("@");
const faro: Coordenada = [43.49, -3.79];
console.log(esEmail("hola@devschool.es"), faro[0]);
// true 43.49
El tipo de las funciones como Validador lo explicamos en Funciones.
Un alias es solo un nombre
Un alias no crea un tipo nuevo: es otra forma de llamar a uno que ya existe. Por eso dos alias con la misma forma son totalmente intercambiables:
type Euros = number;
type Kilos = number;
const precio: Euros = 12;
const peso: Kilos = precio; // TypeScript no protesta: ambos son number
console.log(peso);
// 12
Esto es parte del tipado estructural que viste con las interfaces: lo que cuenta es la forma, no el nombre. Ten en cuenta que el alias no te protege de mezclar euros con kilos; solo hace el código más legible.
Uniones de objetos
Las uniones brillan cuando describen datos que pueden tener formas distintas. Imagina los métodos de pago de una tienda:
type PagoTarjeta = { metodo: "tarjeta"; numero: string; titular: string };
type PagoBizum = { metodo: "bizum"; telefono: string };
type PagoEfectivo = { metodo: "efectivo" };
type Pago = PagoTarjeta | PagoBizum | PagoEfectivo;
function resumen(pago: Pago): string {
switch (pago.metodo) {
case "tarjeta":
return `Tarjeta acabada en ${pago.numero.slice(-4)}`;
case "bizum":
return `Bizum al ${pago.telefono}`;
case "efectivo":
return "Pago en tienda";
}
}
console.log(resumen({ metodo: "tarjeta", numero: "4111222233334444", titular: "Ane" }));
console.log(resumen({ metodo: "bizum", telefono: "600123456" }));
console.log(resumen({ metodo: "efectivo" }));
// Tarjeta acabada en 4444
// Bizum al 600123456
// Pago en tienda
Todas las formas comparten la propiedad metodo con un valor literal distinto. Al comprobarla en el switch, TypeScript sabe en cada case qué forma tiene el pago y te deja usar numero o telefono solo donde existen. Fuera del switch, pago.telefono sería un error, porque no todas las formas lo tienen.
Este patrón se llama unión discriminada y es uno de los más útiles de TypeScript para modelar estados (cargando, con datos, con error), mensajes o acciones. Lo estudiarás en detalle, junto con la comprobación de que no se te olvida ningún caso, en Estrechar tipos.
Intersecciones con &
Si la unión A | B significa “A o B”, la intersección A & B significa “A y B a la vez”: un objeto que tiene todas las propiedades de los dos tipos.
type ConId = { id: number };
type ConFechas = { creado: Date };
type Producto = { nombre: string; precio: number };
type ProductoGuardado = Producto & ConId & ConFechas;
const p: ProductoGuardado = {
id: 7,
nombre: "Esterilla",
precio: 24.95,
creado: new Date("2026-05-10"),
};
console.log(`#${p.id} ${p.nombre}: ${p.precio} €`);
// #7 Esterilla: 24.95 €
Es el equivalente a extends de las interfaces, pero con alias. Si falta cualquiera de las propiedades de alguna de las partes, TypeScript avisa.
Cuidado: si intersectas tipos incompatibles, el resultado es imposible.
string & numberesnever: ningún valor puede ser a la vez texto y número. Si de repente vesneverdonde no lo esperabas, revisa tus intersecciones.
¿type o interface?
Para describir objetos, los dos sirven y la mayoría de las veces el resultado es idéntico. Estas son las diferencias reales:
interface | type | |
|---|---|---|
| Objetos | Sí | Sí |
| Uniones, tuplas, primitivos, funciones sueltas | No | Sí |
| Ampliar | extends | Intersección & |
| Declararla dos veces | Se fusionan | Error: Duplicate identifier |
Una clase la cumple con implements | Sí | Sí, si es un objeto |
| Mensajes de error | Muestran el nombre | A veces muestran el tipo desplegado |
Con estas diferencias, una regla sencilla que siguen muchos equipos:
interfacepara la forma de los objetos de tu aplicación (Usuario,Pedido,Producto).typepara uniones, literales, tuplas, funciones y combinaciones (EstadoPedido,Pago,Coordenada,Validador).
Lo importante es ser coherente dentro del proyecto. Si el equipo usa siempre type, también está bien.
interface Cliente {
nombre: string;
email: string;
}
type Nivel = "basico" | "premium";
interface ClienteFiel extends Cliente {
nivel: Nivel;
puntos: number;
}
const ane: ClienteFiel = { nombre: "Ane", email: "ane@correo.es", nivel: "premium", puntos: 320 };
console.log(`${ane.nombre} (${ane.nivel}): ${ane.puntos} puntos`);
// Ane (premium): 320 puntos
Arrays de uniones
Recuerda de Arrays y tuplas que los paréntesis cambian el significado. Con un alias se evita la confusión:
type Celda = number | null;
const fila: Celda[] = [3, null, 7, null];
const sumaFila = fila.reduce<number>((suma, c) => suma + (c ?? 0), 0);
console.log(sumaFila);
// 10
Celda[] es mucho más fácil de leer que (number | null)[].
Errores frecuentes
- Usar un método de un solo tipo sobre la unión: con
string | numberno puedes llamar atoUpperCase()sin comprobar antes contypeof. - Olvidar
nullen la unión: si un valor puede faltar, decláraloTipo | null; si no,strictno te dejará asignarlenull. - Confundir
|con&:A | Bes “uno de los dos”;A & Bes “los dos a la vez”. En objetos, la intersección tiene más propiedades, no menos. - Pensar que un alias crea un tipo distinto:
type Euros = numbersigue siendonumbery se mezcla con cualquier otronumber. - Escribir
typesin=: estype Id = string | number, notype Id { ... }. Las llaves sin=son deinterface. - Abusar de uniones enormes de primitivos:
string | number | boolean | nullen un parámetro suele indicar que la función hace demasiadas cosas. - Acceder a una propiedad que no está en todas las formas de una unión de objetos: comprueba antes el campo común (
metodo,tipo,estado).
Resumen
| Concepto | Ejemplo |
|---|---|
| Unión | string | number |
| Solo lo común | Con una unión, solo métodos que tengan todos los tipos |
Estrechar con typeof | if (typeof x === "string") { ... } |
| Puede faltar | string | null, con ?. y ?? |
| Unión de literales | "pendiente" | "enviado" | "entregado" |
| Alias | type Id = string | number |
| Alias de cualquier tipo | Tuplas, funciones, objetos, arrays |
| Unión de objetos | type Pago = PagoTarjeta | PagoBizum con campo común |
| Intersección | type Guardado = Producto & ConId |
interface frente a type | interface para objetos; type para el resto |
En la siguiente lección aprenderás a tipar parámetros, valores de retorno y callbacks: Funciones.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.