Explora los tipos 'branded' de TypeScript, una t茅cnica potente para lograr el tipado nominal en un sistema de tipos estructural. Aprende a mejorar la seguridad y claridad del c贸digo.
Tipos 'Branded' en TypeScript: Tipado Nominal en un Sistema Estructural
El sistema de tipos estructural de TypeScript ofrece flexibilidad, pero a veces puede llevar a comportamientos inesperados. Los tipos 'branded' (o de marca) proporcionan una forma de forzar el tipado nominal, mejorando la seguridad de tipos y la claridad del c贸digo. Este art铆culo explora los tipos 'branded' en detalle, proporcionando ejemplos pr谩cticos y mejores pr谩cticas para su implementaci贸n.
Entendiendo el Tipado Estructural vs. Nominal
Antes de sumergirnos en los tipos 'branded', aclaremos la diferencia entre el tipado estructural y el nominal.
Tipado Estructural (Duck Typing)
En un sistema de tipos estructural, dos tipos se consideran compatibles si tienen la misma estructura (es decir, las mismas propiedades con los mismos tipos). TypeScript utiliza el tipado estructural. Considera este ejemplo:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // V谩lido en TypeScript
console.log(vector.x); // Salida: 10
Aunque Point y Vector se declaran como tipos distintos, TypeScript permite asignar un objeto Point a una variable Vector porque comparten la misma estructura. Esto puede ser conveniente, pero tambi茅n puede llevar a errores si necesitas distinguir entre tipos l贸gicamente diferentes que casualmente tienen la misma forma. Por ejemplo, pensar en coordenadas de latitud/longitud que podr铆an coincidir incidentalmente con las coordenadas de p铆xeles de una pantalla.
Tipado Nominal
En un sistema de tipos nominal, los tipos se consideran compatibles solo si tienen el mismo nombre. Incluso si dos tipos tienen la misma estructura, se tratan como distintos si tienen nombres diferentes. Lenguajes como Java y C# utilizan el tipado nominal.
La Necesidad de los Tipos 'Branded'
El tipado estructural de TypeScript puede ser problem谩tico cuando necesitas asegurar que un valor pertenece a un tipo espec铆fico, independientemente de su estructura. Por ejemplo, considera la representaci贸n de monedas. Podr铆as tener diferentes tipos para USD y EUR, pero ambos podr铆an representarse como n煤meros. Sin un mecanismo para distinguirlos, podr铆as realizar accidentalmente operaciones con la moneda equivocada.
Los tipos 'branded' abordan este problema permiti茅ndote crear tipos distintos que son estructuralmente similares pero tratados como diferentes por el sistema de tipos. Esto mejora la seguridad de tipos y previene errores que de otro modo podr铆an pasar desapercibidos.
Implementando Tipos 'Branded' en TypeScript
Los tipos 'branded' se implementan usando tipos de intersecci贸n y un s铆mbolo 煤nico o un literal de cadena. La idea es a帽adir una "marca" a un tipo que lo distinga de otros tipos con la misma estructura.
Usando S铆mbolos (Recomendado)
Generalmente se prefiere usar s铆mbolos para el 'branding' porque se garantiza que los s铆mbolos son 煤nicos.
const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };
const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Descomentar la siguiente l铆nea causar谩 un error de tipo
// const invalidOperation = addUSD(usd1, eur1);
En este ejemplo, USD y EUR son tipos 'branded' basados en el tipo number. El unique symbol asegura que estos tipos sean distintos. Las funciones createUSD y createEUR se utilizan para crear valores de estos tipos, y la funci贸n addUSD solo acepta valores USD. Intentar sumar un valor EUR a un valor USD resultar谩 en un error de tipo.
Usando Literales de Cadena
Tambi茅n puedes usar literales de cadena para el 'branding', aunque este enfoque es menos robusto que usar s铆mbolos porque no se garantiza que los literales de cadena sean 煤nicos.
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Descomentar la siguiente l铆nea causar谩 un error de tipo
// const invalidOperation = addUSD(usd1, eur1);
Este ejemplo logra el mismo resultado que el anterior, pero usando literales de cadena en lugar de s铆mbolos. Aunque es m谩s simple, es importante asegurarse de que los literales de cadena utilizados para el 'branding' sean 煤nicos dentro de tu c贸digo base.
Ejemplos Pr谩cticos y Casos de Uso
Los tipos 'branded' pueden aplicarse a diversos escenarios donde necesitas forzar la seguridad de tipos m谩s all谩 de la compatibilidad estructural.
Identificadores (IDs)
Considera un sistema con diferentes tipos de identificadores, como UserID, ProductID y OrderID. Todos estos IDs podr铆an representarse como n煤meros o cadenas, pero quieres evitar mezclar accidentalmente diferentes tipos de ID.
const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };
const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };
function getUser(id: UserID): { name: string } {
// ... obtener datos del usuario
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... obtener datos del producto
return { name: "Example Product", price: 25 };
}
function createUserID(id: string): UserID {
return id as UserID;
}
function createProductID(id: string): ProductID {
return id as ProductID;
}
const userID = createUserID('user123');
const productID = createProductID('product456');
const user = getUser(userID);
const product = getProduct(productID);
console.log("User:", user);
console.log("Product:", product);
// Descomentar la siguiente l铆nea causar谩 un error de tipo
// const invalidCall = getUser(productID);
Este ejemplo demuestra c贸mo los tipos 'branded' pueden evitar que se pase un ProductID a una funci贸n que espera un UserID, mejorando la seguridad de tipos.
Valores Espec铆ficos del Dominio
Los tipos 'branded' tambi茅n pueden ser 煤tiles para representar valores espec铆ficos del dominio con restricciones. Por ejemplo, podr铆as tener un tipo para porcentajes que siempre deben estar entre 0 y 100.
const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };
function createPercentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error('El porcentaje debe estar entre 0 y 100');
}
return value as Percentage;
}
function applyDiscount(price: number, discount: Percentage): number {
return price * (1 - discount / 100);
}
try {
const discount = createPercentage(20);
const discountedPrice = applyDiscount(100, discount);
console.log("Precio con descuento:", discountedPrice);
// Descomentar la siguiente l铆nea causar谩 un error en tiempo de ejecuci贸n
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Este ejemplo muestra c贸mo forzar una restricci贸n sobre el valor de un tipo 'branded' en tiempo de ejecuci贸n. Aunque el sistema de tipos no puede garantizar que un valor Percentage est茅 siempre entre 0 y 100, la funci贸n createPercentage puede forzar esta restricci贸n en tiempo de ejecuci贸n. Tambi茅n puedes usar librer铆as como io-ts para forzar la validaci贸n en tiempo de ejecuci贸n de los tipos 'branded'.
Representaciones de Fecha y Hora
Trabajar con fechas y horas puede ser complicado debido a los diversos formatos y zonas horarias. Los tipos 'branded' pueden ayudar a diferenciar entre distintas representaciones de fecha y hora.
const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };
const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };
function createUTCDate(dateString: string): UTCDate {
// Validar que la cadena de fecha est茅 en formato UTC (p. ej., ISO 8601 con Z)
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
throw new Error('Formato de fecha UTC inv谩lido');
}
return dateString as UTCDate;
}
function createLocalDate(dateString: string): LocalDate {
// Validar que la cadena de fecha est茅 en formato de fecha local (p. ej., YYYY-MM-DD)
if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
throw new Error('Formato de fecha local inv谩lido');
}
return dateString as LocalDate;
}
function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
// Realizar conversi贸n de zona horaria
const date = new Date(utcDate);
const localDateString = date.toLocaleDateString();
return createLocalDate(localDateString);
}
try {
const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
const localDate = convertUTCDateToLocalDate(utcDate);
console.log("Fecha UTC:", utcDate);
console.log("Fecha Local:", localDate);
} catch (error) {
console.error(error);
}
Este ejemplo diferencia entre fechas UTC y locales, asegurando que est谩s trabajando con la representaci贸n de fecha y hora correcta en diferentes partes de tu aplicaci贸n. La validaci贸n en tiempo de ejecuci贸n asegura que solo las cadenas de fecha con el formato correcto puedan asignarse a estos tipos.
Mejores Pr谩cticas para Usar Tipos 'Branded'
Para usar eficazmente los tipos 'branded' en TypeScript, considera las siguientes mejores pr谩cticas:
- Usa S铆mbolos para el 'Branding': Los s铆mbolos proporcionan la garant铆a m谩s fuerte de unicidad, reduciendo el riesgo de errores de tipo.
- Crea Funciones Auxiliares: Usa funciones auxiliares para crear valores de tipos 'branded'. Esto proporciona un punto central para la validaci贸n y asegura la consistencia.
- Aplica Validaci贸n en Tiempo de Ejecuci贸n: Aunque los tipos 'branded' mejoran la seguridad de tipos, no evitan que se asignen valores incorrectos en tiempo de ejecuci贸n. Usa la validaci贸n en tiempo de ejecuci贸n para forzar las restricciones.
- Documenta los Tipos 'Branded': Documenta claramente el prop贸sito y las restricciones de cada tipo 'branded' para mejorar la mantenibilidad del c贸digo.
- Considera las Implicaciones de Rendimiento: Los tipos 'branded' introducen una peque帽a sobrecarga debido al tipo de intersecci贸n y la necesidad de funciones auxiliares. Considera el impacto en el rendimiento en secciones cr铆ticas de tu c贸digo.
Ventajas de los Tipos 'Branded'
- Seguridad de Tipos Mejorada: Previene la mezcla accidental de tipos estructuralmente similares pero l贸gicamente diferentes.
- Claridad del C贸digo Mejorada: Hace el c贸digo m谩s legible y f谩cil de entender al diferenciar expl铆citamente entre tipos.
- Reducci贸n de Errores: Atrapa errores potenciales en tiempo de compilaci贸n, reduciendo el riesgo de bugs en tiempo de ejecuci贸n.
- Mantenibilidad Aumentada: Hace que el c贸digo sea m谩s f谩cil de mantener y refactorizar al proporcionar una clara separaci贸n de responsabilidades.
Desventajas de los Tipos 'Branded'
- Complejidad Aumentada: A帽ade complejidad a la base del c贸digo, especialmente al tratar con muchos tipos 'branded'.
- Sobrecarga en Tiempo de Ejecuci贸n: Introduce una peque帽a sobrecarga en tiempo de ejecuci贸n debido a la necesidad de funciones auxiliares y validaci贸n en tiempo de ejecuci贸n.
- Potencial para C贸digo Repetitivo (Boilerplate): Puede llevar a c贸digo repetitivo, especialmente al crear y validar tipos 'branded'.
Alternativas a los Tipos 'Branded'
Aunque los tipos 'branded' son una t茅cnica potente para lograr el tipado nominal en TypeScript, existen enfoques alternativos que podr铆as considerar.
Tipos Opacos
Los tipos opacos son similares a los tipos 'branded' pero proporcionan una forma m谩s expl铆cita de ocultar el tipo subyacente. TypeScript no tiene soporte nativo para tipos opacos, pero puedes simularlos usando m贸dulos y s铆mbolos privados.
Clases
Usar clases puede proporcionar un enfoque m谩s orientado a objetos para definir tipos distintos. Aunque las clases tienen tipado estructural en TypeScript, ofrecen una separaci贸n de responsabilidades m谩s clara y pueden usarse para forzar restricciones a trav茅s de m茅todos.
Librer铆as como `io-ts` o `zod`
Estas librer铆as proporcionan una validaci贸n de tipos sofisticada en tiempo de ejecuci贸n y pueden combinarse con tipos 'branded' para asegurar la seguridad tanto en tiempo de compilaci贸n como de ejecuci贸n.
Conclusi贸n
Los tipos 'branded' de TypeScript son una herramienta valiosa para mejorar la seguridad de tipos y la claridad del c贸digo en un sistema de tipos estructural. Al a帽adir una "marca" a un tipo, puedes forzar el tipado nominal y prevenir la mezcla accidental de tipos estructuralmente similares pero l贸gicamente diferentes. Aunque los tipos 'branded' introducen cierta complejidad y sobrecarga, los beneficios de una mayor seguridad de tipos y mantenibilidad del c贸digo a menudo superan las desventajas. Considera usar tipos 'branded' en escenarios donde necesites asegurar que un valor pertenece a un tipo espec铆fico, independientemente de su estructura.
Al entender los principios detr谩s del tipado estructural y nominal, y al aplicar las mejores pr谩cticas descritas en este art铆culo, puedes aprovechar eficazmente los tipos 'branded' para escribir c贸digo TypeScript m谩s robusto y mantenible. Desde representar monedas e IDs hasta forzar restricciones espec铆ficas del dominio, los tipos 'branded' proporcionan un mecanismo flexible y potente para mejorar la seguridad de tipos en tus proyectos.
A medida que trabajes con TypeScript, explora las diversas t茅cnicas y librer铆as disponibles para la validaci贸n y el forzado de tipos. Considera usar tipos 'branded' junto con librer铆as de validaci贸n en tiempo de ejecuci贸n como io-ts o zod para lograr un enfoque integral de la seguridad de tipos.