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:
-
Acceso a propiedades específicas: Como vimos anteriormente, solo podemos acceder directamente a propiedades comunes a todos los tipos de la unión.
-
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.
¿Te está gustando esta lección?
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
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:
- Varios tipos (normalmente interfaces o tipos)
- Una propiedad discriminadora común con valores literales diferentes
- 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:
- Flexibilidad: Permiten modelar tipos que tienen estructuras completamente diferentes.
- Seguridad de tipos: El compilador garantiza que accedamos solo a propiedades válidas para cada variante.
- Exhaustividad: Podemos verificar que todos los casos posibles sean manejados.
- 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.
Aprendizajes 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.
Completa TypeScript y certifícate
Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs