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ícateUniones 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.
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.
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
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Proyecto Inventario de productos
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
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
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras Y Efectos Secundarios
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales Y Tipos Condicionales
Tipos Intermedios Y Avanzados
Tipos Genéricos Básicos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad (Partial, Required, Pick, Etc)
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
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.