TypeScript

TypeScript

Tutorial TypeScript: Tipos de unión e intersección

Aprende los tipos de unión e intersección en TypeScript para manejar datos flexibles y seguros con ejemplos prácticos y técnicas avanzadas.

Aprende TypeScript y certifícate

Uniones con operador |

Los tipos de unión representan uno de los conceptos más potentes y flexibles de TypeScript, permitiéndonos expresar que un valor puede ser de varios tipos diferentes. El operador de unión, representado por el símbolo |, nos permite combinar dos o más tipos en uno solo.

Un tipo de unión describe un valor que puede ser de cualquiera de los tipos especificados. Esto resulta extremadamente útil cuando necesitamos manejar diferentes tipos de datos en una misma variable o parámetro.

Sintaxis básica

La sintaxis para crear un tipo de unión es sencilla:

let variable: TipoA | TipoB;

Esto indica que variable puede contener un valor de tipo TipoA o de tipo TipoB.

Casos de uso comunes

1. Variables que pueden tener diferentes tipos

// Una variable que puede ser string o number
let id: string | number;

id = 101;     // Válido
id = "A101";  // También válido
id = true;    // Error: Type 'boolean' is not assignable to type 'string | number'

2. Parámetros de función flexibles

// Una función que acepta un string o un array de strings
function printId(id: string | string[]): void {
  if (Array.isArray(id)) {
    console.log("ID array: " + id.join(", "));
  } else {
    console.log("ID: " + id);
  }
}

printId("abc123");        // Válido
printId(["abc", "123"]);  // Válido

3. Valores opcionales con null o undefined

// Una variable que puede contener un string o ser null
let userName: string | null = "user1";

// Más tarde en el código
userName = null;  // Válido cuando el usuario cierra sesión

Uniones con tipos literales

Las uniones son especialmente útiles cuando se combinan con tipos literales, permitiendo crear conjuntos específicos de valores permitidos:

// Un tipo que representa estados específicos de un proceso
type Status = "pending" | "processing" | "completed" | "failed";

let currentStatus: Status = "pending";
currentStatus = "completed";  // Válido
currentStatus = "unknown";    // Error: Type '"unknown"' is not assignable to type 'Status'

Este patrón es muy común para representar estados, niveles de acceso, o cualquier conjunto finito de opciones.

Uniones con interfaces y tipos personalizados

También podemos crear uniones con tipos más complejos como interfaces:

interface Employee {
  id: number;
  name: string;
}

interface Customer {
  id: string;
  name: string;
  email: string;
}

// Una función que puede procesar tanto empleados como clientes
function processUser(user: Employee | Customer): void {
  console.log("Nombre: " + user.name);
  
  // Acceso a propiedades específicas requiere verificación
  if ("email" in user) {
    console.log("Email: " + user.email);
  }
}

Accediendo a propiedades en uniones

Al trabajar con uniones de tipos complejos, solo podemos acceder directamente a las propiedades comunes a todos los tipos de la unión sin realizar comprobaciones:

interface Car {
  make: string;
  model: string;
  year: number;
  startEngine(): void;
}

interface Bicycle {
  make: string;
  model: string;
  type: "mountain" | "road" | "hybrid";
  startPedaling(): void;
}

function describeVehicle(vehicle: Car | Bicycle) {
  // Propiedades comunes - acceso directo
  console.log(`${vehicle.make} ${vehicle.model}`);
  
  // Propiedades específicas - requieren verificación
  if ("year" in vehicle) {
    console.log(`Año: ${vehicle.year}`);
    vehicle.startEngine();
  } else {
    console.log(`Tipo: ${vehicle.type}`);
    vehicle.startPedaling();
  }
}

Uniones con tipos genéricos

Las uniones también pueden combinarse con tipos genéricos para crear estructuras de datos flexibles:

// Un resultado que puede contener datos o un error
type Result<T> = { success: true; data: T } | { success: false; error: string };

function fetchUserData(userId: string): Result<{ name: string; email: string }> {
  // Simulación de una petición
  if (userId === "valid") {
    return {
      success: true,
      data: { name: "John Doe", email: "john@example.com" }
    };
  } else {
    return {
      success: false,
      error: "Usuario no encontrado"
    };
  }
}

const result = fetchUserData("valid");
if (result.success) {
  // TypeScript sabe que result.data existe aquí
  console.log(result.data.name);
} else {
  // TypeScript sabe que result.error existe aquí
  console.log(result.error);
}

Limitaciones de las uniones

Aunque las uniones son muy útiles, tienen algunas limitaciones:

  1. Acceso a propiedades específicas: Como vimos anteriormente, solo podemos acceder directamente a propiedades comunes a todos los tipos de la unión.

  2. Operaciones específicas: No podemos realizar operaciones específicas de un tipo sin antes verificar el tipo.

function calculateLength(value: string | string[]): number {
  // Esto generaría un error porque .length tiene diferentes significados
  // en string y string[]
  // return value.length; // Error
  
  // Necesitamos verificar el tipo primero
  if (typeof value === "string") {
    return value.length;
  } else {
    return value.reduce((acc, str) => acc + str.length, 0);
  }
}

Uniones vs. any

Es importante distinguir entre usar uniones y usar el tipo any:

// Con unión - TypeScript verifica que solo se asignen los tipos permitidos
let id: string | number;
id = 123;      // OK
id = "abc";    // OK
id = true;     // Error

// Con any - TypeScript no realiza verificaciones
let idAny: any;
idAny = 123;   // OK
idAny = "abc"; // OK
idAny = true;  // OK - pero podría causar errores en tiempo de ejecución

Las uniones proporcionan flexibilidad con seguridad de tipos, mientras que any elimina completamente la verificación de tipos.

Intersecciones con operador &

Las intersecciones de tipos representan una característica fundamental en TypeScript que complementa a las uniones. Mientras que las uniones (con el operador |) permiten que un valor sea de uno u otro tipo, las intersecciones (con el operador &) combinan múltiples tipos en uno solo que contiene todas las propiedades de los tipos combinados.

Un tipo de intersección crea un nuevo tipo que tiene todas las características de cada uno de los tipos que lo componen. Esto resulta especialmente útil cuando necesitamos combinar interfaces o tipos existentes.

Sintaxis básica

La sintaxis para crear un tipo de intersección es directa:

type CombinedType = TypeA & TypeB;

Esto indica que CombinedType tendrá todas las propiedades y métodos tanto de TypeA como de TypeB.

Casos de uso comunes

1. Combinación de interfaces

interface Identificable {
  id: string;
  label: string;
}

interface Validable {
  isValid(): boolean;
  validate(): void;
}

// Combinamos ambas interfaces
type ValidatableItem = Identificable & Validable;

// Un objeto debe implementar todas las propiedades de ambas interfaces
const formField: ValidatableItem = {
  id: "email-field",
  label: "Correo electrónico",
  isValid() {
    return this.id.length > 0;
  },
  validate() {
    console.log(`Validando ${this.label}...`);
  }
};

2. Extensión de tipos existentes

type BaseAddress = {
  street: string;
  city: string;
};

type AddressWithRegion = BaseAddress & {
  region: string;
};

type InternationalAddress = AddressWithRegion & {
  country: string;
  postalCode: string;
};

const address: InternationalAddress = {
  street: "Calle Mayor 123",
  city: "Madrid",
  region: "Comunidad de Madrid",
  country: "España",
  postalCode: "28001"
};

3. Mixins de comportamiento

Las intersecciones son ideales para implementar el patrón de mixins, donde combinamos múltiples comportamientos:

type Loggable = {
  log(message: string): void;
};

type Serializable = {
  serialize(): string;
};

type Persistable = {
  save(): void;
  load(): void;
};

// Creamos un tipo que combina todos estos comportamientos
type DataObject = Loggable & Serializable & Persistable;

class ConfigurationManager implements DataObject {
  private data: Record<string, any> = {};
  
  log(message: string): void {
    console.log(`[ConfigManager] ${message}`);
  }
  
  serialize(): string {
    return JSON.stringify(this.data);
  }
  
  save(): void {
    const data = this.serialize();
    this.log(`Guardando datos: ${data}`);
    localStorage.setItem('config', data);
  }
  
  load(): void {
    const stored = localStorage.getItem('config');
    if (stored) {
      this.data = JSON.parse(stored);
      this.log('Configuración cargada');
    }
  }
}

Intersecciones con tipos primitivos

Cuando intersecamos tipos primitivos, TypeScript calcula el tipo común más específico:

type NumberAndString = number & string;

En este caso, NumberAndString es en realidad el tipo never, porque no existe ningún valor que pueda ser simultáneamente un número y una cadena.

Sin embargo, las intersecciones son útiles con tipos literales:

type PositiveEvenNumber = number & { __brand: "PositiveEven" };

function createPositiveEven(n: number): PositiveEvenNumber {
  if (n <= 0 || n % 2 !== 0) {
    throw new Error("El número debe ser positivo y par");
  }
  return n as PositiveEvenNumber;
}

// Uso del tipo con "branded types"
const evenNumber = createPositiveEven(4);

Intersecciones vs. herencia

Las intersecciones proporcionan una alternativa a la herencia tradicional en la programación orientada a objetos:

// Enfoque de herencia
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Swimmer extends Animal {
  swim() {
    console.log(`${this.name} está nadando`);
  }
}

// Enfoque de intersección
type Animal = {
  name: string;
};

type Swimmer = {
  swim(): void;
};

type Fish = Animal & Swimmer;

const nemo: Fish = {
  name: "Nemo",
  swim() {
    console.log(`${this.name} está nadando`);
  }
};

La ventaja del enfoque de intersección es que permite combinar tipos de múltiples fuentes sin necesidad de una jerarquía de clases.

Resolución de conflictos en propiedades

Cuando dos tipos en una intersección tienen propiedades con el mismo nombre pero tipos diferentes, TypeScript crea una intersección de esos tipos de propiedades:

type A = { prop: number };
type B = { prop: string };

type AB = A & B;

// Esto no funcionará porque no existe un valor que sea
// simultáneamente number y string
const ab: AB = {
  prop: 5 // Error: Type 'number' is not assignable to type 'never'
};

En este caso, prop en el tipo AB es de tipo number & string, que se reduce a never.

Intersecciones con genéricos

Las intersecciones son especialmente potentes cuando se combinan con genéricos:

type WithId<T> = T & { id: string };

interface User {
  name: string;
  email: string;
}

// Ahora podemos crear usuarios con ID garantizado
const user: WithId<User> = {
  id: "user-123",
  name: "Ana García",
  email: "ana@example.com"
};

// Funciona con cualquier tipo
interface Product {
  title: string;
  price: number;
}

const product: WithId<Product> = {
  id: "prod-456",
  title: "Teclado mecánico",
  price: 89.99
};

Uso práctico: patrón de extensión de configuración

Las intersecciones son ideales para implementar patrones de configuración extensible:

// Configuración base con valores por defecto
interface BaseConfig {
  endpoint: string;
  timeout: number;
  retries: number;
}

// Configuraciones específicas para diferentes entornos
interface DevConfig {
  mockResponses: boolean;
  logLevel: 'debug' | 'info';
}

interface ProdConfig {
  sslEnabled: boolean;
  cacheTime: number;
}

// Configuraciones combinadas
type DevelopmentConfig = BaseConfig & DevConfig;
type ProductionConfig = BaseConfig & ProdConfig;

function createApiClient(config: DevelopmentConfig | ProductionConfig) {
  // Implementación del cliente...
}

// Uso
const devConfig: DevelopmentConfig = {
  endpoint: 'http://dev-api.example.com',
  timeout: 5000,
  retries: 3,
  mockResponses: true,
  logLevel: 'debug'
};

createApiClient(devConfig);

Intersecciones vs. uniones discriminadas

Es importante entender la diferencia entre intersecciones y uniones discriminadas:

// Intersección: un objeto que es AMBOS tipos a la vez
type ButtonProps = CommonProps & (PrimaryButtonProps | SecondaryButtonProps);

// Unión discriminada: un objeto que es UNO U OTRO tipo
type ButtonVariant = 
  | { variant: 'primary'; color: string }
  | { variant: 'secondary'; outline: boolean };

Las intersecciones son útiles cuando queremos combinar características, mientras que las uniones discriminadas son mejores cuando queremos representar alternativas.

Estrechamiento de tipos (type narrowing)

El estrechamiento de tipos (type narrowing) es una técnica fundamental en TypeScript que permite refinar el tipo de una variable dentro de un bloque de código específico. Cuando trabajamos con tipos de unión, TypeScript necesita mecanismos para determinar qué tipo específico tiene una variable en un momento dado del programa.

Esta técnica resulta esencial cuando manejamos uniones de tipos, ya que nos permite acceder de forma segura a propiedades y métodos específicos de cada tipo después de verificar su naturaleza.

Operador typeof

El operador typeof es la forma más básica de estrechamiento de tipos, permitiéndonos distinguir entre tipos primitivos:

function printValue(value: string | number) {
  if (typeof value === "string") {
    // En este bloque, TypeScript sabe que value es un string
    console.log(value.toUpperCase());
  } else {
    // En este bloque, TypeScript sabe que value es un number
    console.log(value.toFixed(2));
  }
}

TypeScript reconoce este patrón y estrecha el tipo de value dentro de cada rama condicional, permitiéndonos acceder a métodos específicos de cada tipo sin errores de compilación.

Operador instanceof

Para trabajar con clases e instancias de objetos, podemos usar el operador instanceof:

class Customer {
  name: string;
  email: string;
  
  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
  
  sendEmail() {
    console.log(`Enviando email a ${this.email}`);
  }
}

class Employee {
  name: string;
  department: string;
  
  constructor(name: string, department: string) {
    this.name = name;
    this.department = department;
  }
  
  assignTask() {
    console.log(`Asignando tarea a ${this.name} del departamento ${this.department}`);
  }
}

function processUser(user: Customer | Employee) {
  console.log(`Procesando usuario: ${user.name}`);
  
  if (user instanceof Customer) {
    // TypeScript sabe que user es Customer aquí
    user.sendEmail();
  } else {
    // TypeScript sabe que user es Employee aquí
    user.assignTask();
  }
}

Comprobación de propiedades con in

El operador in nos permite verificar si una propiedad existe en un objeto, lo que resulta útil para distinguir entre diferentes interfaces:

interface Article {
  title: string;
  content: string;
  publishDate: Date;
}

interface Video {
  title: string;
  duration: number;
  resolution: string;
}

function displayMedia(media: Article | Video) {
  console.log(`Título: ${media.title}`);
  
  if ("content" in media) {
    // TypeScript sabe que media es Article
    console.log(`Contenido: ${media.content.substring(0, 100)}...`);
    console.log(`Publicado: ${media.publishDate.toLocaleDateString()}`);
  } else {
    // TypeScript sabe que media es Video
    console.log(`Duración: ${media.duration} segundos`);
    console.log(`Resolución: ${media.resolution}`);
  }
}

Predicados de tipo (type predicates)

Los predicados de tipo son funciones que devuelven un valor booleano y tienen una firma especial que le indica a TypeScript cómo debe estrechar el tipo:

// Definimos un predicado de tipo con la sintaxis "paramName is Type"
function isArticle(media: Article | Video): media is Article {
  return (media as Article).content !== undefined;
}

function displayMediaWithPredicate(media: Article | Video) {
  if (isArticle(media)) {
    // TypeScript sabe que media es Article aquí
    console.log(`Extracto: ${media.content.substring(0, 50)}...`);
  } else {
    // TypeScript sabe que media es Video aquí
    console.log(`Ver video (${media.duration}s): ${media.title}`);
  }
}

Los predicados de tipo son especialmente útiles cuando la lógica de verificación es compleja o se necesita reutilizar en varios lugares.

Aserciones de tipo exhaustivas

Una técnica avanzada es usar el tipo never para asegurarnos de que todos los casos posibles han sido manejados:

type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      // Esta función nunca debería ejecutarse si hemos manejado todos los casos
      const exhaustiveCheck: never = shape;
      throw new Error(`Tipo de forma no manejado: ${exhaustiveCheck}`);
  }
}

Si añadimos un nuevo tipo a Shape pero olvidamos actualizar la función calculateArea, TypeScript generará un error en tiempo de compilación.

Estrechamiento con asignación

TypeScript también estrecha tipos cuando asignamos valores a variables:

let id: string | number;

// Asignación directa
id = "abc123";
// TypeScript sabe que id es string aquí
console.log(id.toUpperCase());

// Nueva asignación
id = 456;
// TypeScript sabe que id es number aquí
console.log(id.toFixed());

Estrechamiento con operadores lógicos

Los operadores lógicos como && y || también participan en el estrechamiento de tipos:

function processValue(value: string | null | undefined) {
  // El operador && estrecha el tipo eliminando null y undefined
  if (value && value.length > 5) {
    console.log(`Valor largo: ${value}`);
  }
  
  // Usando el operador de coalescencia nula
  const safeValue = value ?? "valor predeterminado";
  // TypeScript sabe que safeValue es string aquí
  console.log(safeValue.toUpperCase());
}

Estrechamiento con expresiones de igualdad

Las comparaciones de igualdad también pueden estrechar tipos:

function printId(id: string | number | null) {
  if (id === null) {
    console.log("No hay ID disponible");
    return;
  }
  
  // TypeScript sabe que id es string | number aquí
  console.log(`ID: ${id}`);
  
  // Podemos seguir estrechando
  if (typeof id === "string") {
    console.log(`ID en mayúsculas: ${id.toUpperCase()}`);
  }
}

Estrechamiento con tipos literales

El estrechamiento funciona especialmente bien con tipos literales:

type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction) {
  switch (direction) {
    case "north":
      console.log("Moviendo hacia el norte");
      break;
    case "south":
      console.log("Moviendo hacia el sur");
      break;
    case "east":
      console.log("Moviendo hacia el este");
      break;
    case "west":
      console.log("Moviendo hacia el oeste");
      break;
  }
}

Estrechamiento con genéricos

El estrechamiento de tipos también funciona con tipos genéricos:

function firstElement<T>(arr: T[]): T | undefined {
  if (arr.length === 0) {
    return undefined;
  }
  
  return arr[0];
}

const result = firstElement([1, 2, 3]);
if (result !== undefined) {
  // TypeScript sabe que result es number aquí
  console.log(result.toFixed(2));
}

Limitaciones del estrechamiento de tipos

Es importante entender algunas limitaciones:

function example(x: string | number) {
  // Esta función auxiliar no conserva el estrechamiento
  const isString = typeof x === "string";
  
  if (isString) {
    // Error: TypeScript no puede estrechar x aquí
    // console.log(x.toUpperCase());
  }
  
  // La forma correcta:
  if (typeof x === "string") {
    console.log(x.toUpperCase());
  }
}

TypeScript solo puede realizar estrechamiento cuando la condición está directamente en el flujo de control, no cuando se almacena en variables intermedias.

Estrechamiento con tipos de usuario

Podemos crear nuestros propios mecanismos de estrechamiento para tipos personalizados:

interface SuccessResponse {
  status: "success";
  data: any;
}

interface ErrorResponse {
  status: "error";
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    // TypeScript sabe que response es SuccessResponse
    console.log(`Datos recibidos: ${JSON.stringify(response.data)}`);
  } else {
    // TypeScript sabe que response es ErrorResponse
    console.log(`Error: ${response.message}`);
  }
}

El estrechamiento de tipos es una herramienta fundamental que permite escribir código TypeScript más seguro y expresivo, especialmente cuando trabajamos con uniones de tipos complejas.

Discriminated unions

Las discriminated unions (uniones discriminadas) representan un patrón de diseño de tipos en TypeScript que combina uniones de tipos con propiedades discriminadoras para crear estructuras de datos más seguras y expresivas. Este patrón es fundamental para modelar datos que pueden existir en diferentes estados o variantes.

Una unión discriminada se caracteriza por tener una propiedad común (el discriminador) cuyo valor literal permite a TypeScript determinar exactamente qué variante de la unión está siendo utilizada en un momento dado. Esto facilita enormemente el manejo de datos complejos con múltiples formas posibles.

Estructura básica

La estructura de una unión discriminada consta de:

  1. Varios tipos (normalmente interfaces o tipos)
  2. Una propiedad discriminadora común con valores literales diferentes
  3. Propiedades específicas para cada variante
type Circle = {
  kind: "circle";  // Propiedad discriminadora
  radius: number;  // Propiedades específicas
};

type Rectangle = {
  kind: "rectangle";  // Mismo nombre de propiedad, valor diferente
  width: number;      // Propiedades específicas
  height: number;
};

// La unión discriminada
type Shape = Circle | Rectangle;

En este ejemplo, kind es la propiedad discriminadora que permite a TypeScript distinguir entre un círculo y un rectángulo.

Trabajando con uniones discriminadas

La principal ventaja de las uniones discriminadas es que TypeScript puede inferir automáticamente el tipo específico después de comprobar el valor del discriminador:

function calculateArea(shape: Shape): number {
  // Comprobando el discriminador
  if (shape.kind === "circle") {
    // TypeScript sabe que shape es Circle aquí
    return Math.PI * shape.radius ** 2;
  } else {
    // TypeScript sabe que shape es Rectangle aquí
    return shape.width * shape.height;
  }
}

const myCircle: Shape = { kind: "circle", radius: 5 };
console.log(calculateArea(myCircle));  // 78.54...

Uso con switch

Las uniones discriminadas funcionan especialmente bien con declaraciones switch, proporcionando una sintaxis clara y comprobaciones de exhaustividad:

// Ampliamos nuestra unión con un nuevo tipo
type Triangle = {
  kind: "triangle";
  base: number;
  height: number;
};

// Unión discriminada actualizada
type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
  // Usando switch con el discriminador
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      // Comprobación de exhaustividad
      const _exhaustiveCheck: never = shape;
      throw new Error(`Tipo no soportado: ${_exhaustiveCheck}`);
  }
}

La variable _exhaustiveCheck garantiza que si añadimos un nuevo tipo a Shape pero olvidamos actualizarlo en la función, obtendremos un error de compilación.

Discriminadores y propiedades específicas

Cada variante en una unión discriminada puede tener propiedades completamente diferentes, adaptadas a su propósito específico:

type LoginEvent = {
  type: "login";
  userId: string;
  timestamp: Date;
};

type LogoutEvent = {
  type: "logout";
  userId: string;
  timestamp: Date;
};

type PasswordChangeEvent = {
  type: "passwordChange";
  userId: string;
  timestamp: Date;
  newPasswordStrength: "weak" | "medium" | "strong";
};

type UserEvent = LoginEvent | LogoutEvent | PasswordChangeEvent;

function handleUserEvent(event: UserEvent) {
  // Propiedades comunes accesibles directamente
  console.log(`Evento para usuario ${event.userId} a las ${event.timestamp.toISOString()}`);
  
  // Propiedades específicas requieren comprobación del discriminador
  switch (event.type) {
    case "login":
      console.log("Usuario ha iniciado sesión");
      break;
    case "logout":
      console.log("Usuario ha cerrado sesión");
      break;
    case "passwordChange":
      console.log(`Contraseña cambiada. Fortaleza: ${event.newPasswordStrength}`);
      break;
  }
}

Discriminadores múltiples

Aunque lo más común es usar un solo discriminador, podemos utilizar múltiples propiedades para discriminar entre tipos:

type AdminUser = {
  role: "admin";
  permissions: string[];
  department: string;
};

type RegularUser = {
  role: "user";
  subscription: "free" | "premium";
};

type GuestUser = {
  role: "guest";
  expirationDate: Date;
};

type User = AdminUser | RegularUser | GuestUser;

function getUserInfo(user: User): string {
  // Discriminación basada en la propiedad 'role'
  switch (user.role) {
    case "admin":
      return `Administrador del departamento ${user.department}`;
    case "user":
      return `Usuario con suscripción ${user.subscription}`;
    case "guest":
      const daysLeft = Math.ceil((user.expirationDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
      return `Invitado (expira en ${daysLeft} días)`;
  }
}

Discriminadores con tipos literales

Los discriminadores no tienen que ser strings; pueden ser cualquier tipo literal:

type Success = {
  status: 200;
  data: unknown;
  headers: Record<string, string>;
};

type ClientError = {
  status: 400 | 401 | 404;
  error: string;
};

type ServerError = {
  status: 500;
  error: string;
  stackTrace: string;
};

type ApiResponse = Success | ClientError | ServerError;

function handleResponse(response: ApiResponse) {
  // Usando un número como discriminador
  if (response.status === 200) {
    console.log("Éxito:", response.data);
  } else if (response.status >= 400 && response.status < 500) {
    console.log("Error del cliente:", response.error);
  } else {
    console.log("Error del servidor:", response.error);
    console.log("Stack trace:", response.stackTrace);
  }
}

Uniones discriminadas con genéricos

Podemos combinar uniones discriminadas con genéricos para crear patrones reutilizables:

type Result<T, E = Error> = 
  | { status: "success"; value: T }
  | { status: "error"; error: E };

function fetchUserData(userId: string): Result<{name: string, email: string}> {
  try {
    // Simulación de una petición
    if (userId === "123") {
      return {
        status: "success",
        value: { name: "Ana García", email: "ana@example.com" }
      };
    }
    throw new Error("Usuario no encontrado");
  } catch (error) {
    return {
      status: "error",
      error: error instanceof Error ? error : new Error(String(error))
    };
  }
}

const result = fetchUserData("123");

if (result.status === "success") {
  // TypeScript sabe que result.value existe y es del tipo correcto
  console.log(`Usuario: ${result.value.name}, Email: ${result.value.email}`);
} else {
  // TypeScript sabe que result.error existe
  console.log(`Error: ${result.error.message}`);
}

Aplicaciones prácticas

Las uniones discriminadas son especialmente útiles en varios escenarios:

1. Gestión de estados en aplicaciones

type LoadingState = {
  status: "loading";
};

type SuccessState<T> = {
  status: "success";
  data: T;
};

type ErrorState = {
  status: "error";
  error: string;
};

type State<T> = LoadingState | SuccessState<T> | ErrorState;

// Componente que renderiza diferente UI según el estado
function renderData(state: State<string[]>) {
  switch (state.status) {
    case "loading":
      return "Cargando datos...";
    case "success":
      return `Datos: ${state.data.join(", ")}`;
    case "error":
      return `Error: ${state.error}`;
  }
}

2. Modelado de mensajes en sistemas de comunicación

type TextMessage = {
  kind: "text";
  content: string;
};

type ImageMessage = {
  kind: "image";
  url: string;
  dimensions: { width: number; height: number };
};

type VideoMessage = {
  kind: "video";
  url: string;
  duration: number;
};

type Message = TextMessage | ImageMessage | VideoMessage;

function renderMessage(message: Message) {
  switch (message.kind) {
    case "text":
      return `<div class="text-message">${message.content}</div>`;
    case "image":
      return `<img src="${message.url}" width="${message.dimensions.width}" height="${message.dimensions.height}" />`;
    case "video":
      return `<video src="${message.url}" controls></video><span>${message.duration}s</span>`;
  }
}

3. Modelado de acciones en sistemas de reducción de estado

type AddTodoAction = {
  type: "ADD_TODO";
  text: string;
};

type ToggleTodoAction = {
  type: "TOGGLE_TODO";
  id: number;
};

type DeleteTodoAction = {
  type: "DELETE_TODO";
  id: number;
};

type TodoAction = AddTodoAction | ToggleTodoAction | DeleteTodoAction;

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

function todoReducer(state: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, {
        id: Date.now(),
        text: action.text,
        completed: false
      }];
    case "TOGGLE_TODO":
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    case "DELETE_TODO":
      return state.filter(todo => todo.id !== action.id);
  }
}

Ventajas sobre interfaces y herencia

Las uniones discriminadas ofrecen varias ventajas sobre la herencia tradicional:

  1. Flexibilidad: Permiten modelar tipos que tienen estructuras completamente diferentes.
  2. Seguridad de tipos: El compilador garantiza que accedamos solo a propiedades válidas para cada variante.
  3. Exhaustividad: Podemos verificar que todos los casos posibles sean manejados.
  4. Composición sobre herencia: Promueven un enfoque de composición que suele ser más flexible.
// Enfoque con herencia (menos flexible)
abstract class Vehicle {
  abstract getDescription(): string;
}

class Car extends Vehicle {
  constructor(public doors: number) { super(); }
  
  getDescription() {
    return `Coche con ${this.doors} puertas`;
  }
}

class Bicycle extends Vehicle {
  constructor(public type: string) { super(); }
  
  getDescription() {
    return `Bicicleta tipo ${this.type}`;
  }
}

// Enfoque con unión discriminada (más flexible)
type Car = {
  kind: "car";
  doors: number;
};

type Bicycle = {
  kind: "bicycle";
  type: string;
};

type Vehicle = Car | Bicycle;

function getDescription(vehicle: Vehicle): string {
  switch (vehicle.kind) {
    case "car":
      return `Coche con ${vehicle.doors} puertas`;
    case "bicycle":
      return `Bicicleta tipo ${vehicle.type}`;
  }
}

Las uniones discriminadas son una herramienta esencial en el arsenal de TypeScript, permitiendo modelar datos complejos con múltiples variantes de manera segura y expresiva. Combinadas con el estrechamiento de tipos, proporcionan una forma robusta de manejar diferentes casos en tu código con total seguridad de tipos.

Aprende TypeScript online

Otros ejercicios de programación de TypeScript

Evalúa tus conocimientos de esta lección Tipos de unión e intersección con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Proyecto Inventario de productos

TypeScript
Proyecto

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

Todas las lecciones de TypeScript

Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Typescript

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras Y Efectos Secundarios

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales Y Tipos Condicionales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos Básicos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad (Partial, Required, Pick, Etc)

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el uso y sintaxis de los tipos de unión con el operador | para representar valores que pueden ser de varios tipos.
  • Aprender a combinar tipos mediante intersecciones con el operador & para crear tipos que contienen todas las propiedades de los tipos combinados.
  • Entender el concepto de estrechamiento de tipos (type narrowing) para refinar tipos dentro de bloques condicionales y acceder a propiedades específicas.
  • Conocer las uniones discriminadas como patrón para modelar variantes de tipos con propiedades discriminadoras y manejar casos de forma segura y exhaustiva.
  • Diferenciar entre uniones, intersecciones, uniones discriminadas y el tipo any, y aplicar cada uno según el contexto adecuado.