El problema del tipado estructural
TypeScript utiliza un sistema de tipado estructural: dos tipos son compatibles si tienen la misma estructura, independientemente de su nombre. Esto funciona bien en la mayoría de casos, pero genera problemas cuando valores con la misma estructura representan conceptos diferentes.

type UserId = string
type OrderId = string
function obtenerUsuario(id: UserId): void {
console.log(`Buscando usuario: ${id}`)
}
function obtenerPedido(id: OrderId): void {
console.log(`Buscando pedido: ${id}`)
}
const userId: UserId = "usr_123"
const orderId: OrderId = "ord_456"
// TypeScript NO detecta este error: ambos son string
obtenerUsuario(orderId) // Compila sin errores, pero es incorrecto
obtenerPedido(userId) // Compila sin errores, pero es incorrecto
Ambos alias son string, por lo que TypeScript los considera intercambiables. En un sistema real, confundir un identificador de usuario con uno de pedido puede provocar errores graves que solo se detectan en tiempo de ejecución.
El tipado estructural de TypeScript es util para la mayoría de escenarios, pero cuando necesitas distinguir tipos que comparten estructura, los branded types aportan seguridad nominal sin cambiar el sistema de tipos base.
Crear branded types con unique symbol
La técnica más robusta para crear branded types utiliza unique symbol como propiedad fantasma. Esta propiedad nunca existe en tiempo de ejecución, pero el compilador la considera parte del tipo:
declare const BrandUserId: unique symbol
declare const BrandOrderId: unique symbol
type UserId = string & { readonly [BrandUserId]: typeof BrandUserId }
type OrderId = string & { readonly [BrandOrderId]: typeof BrandOrderId }
function crearUserId(valor: string): UserId {
return valor as UserId
}
function crearOrderId(valor: string): OrderId {
return valor as OrderId
}
function obtenerUsuario(id: UserId): void {
console.log(`Buscando usuario: ${id}`)
}
function obtenerPedido(id: OrderId): void {
console.log(`Buscando pedido: ${id}`)
}
const userId = crearUserId("usr_123")
const orderId = crearOrderId("ord_456")
obtenerUsuario(userId) // Correcto
obtenerPedido(orderId) // Correcto
// obtenerUsuario(orderId) // Error de compilación
// obtenerPedido(userId) // Error de compilación
Cada unique symbol genera un tipo único e irrepetible, garantizando que UserId y OrderId sean incompatibles aunque ambos extiendan string.
Patrón genérico con Brand
Para evitar repetir la declaración de símbolos, se puede crear un tipo genérico reutilizable:
declare const __brand: unique symbol
type Brand<T, B> = T & { readonly [__brand]: B }
type UserId = Brand<string, "UserId">
type OrderId = Brand<string, "OrderId">
type ProductId = Brand<number, "ProductId">
function crearUserId(valor: string): UserId {
return valor as UserId
}
function crearOrderId(valor: string): OrderId {
return valor as OrderId
}
function crearProductId(valor: number): ProductId {
return valor as ProductId
}
const userId = crearUserId("usr_001")
const orderId = crearOrderId("ord_002")
const productId = crearProductId(42)
// Cada tipo es incompatible con los demas
// const error: UserId = orderId // Error de compilación
// const error2: ProductId = 42 // Error: number no es ProductId
Branded types con __brand
Una alternativa más sencilla utiliza una propiedad __brand con un tipo literal:
type UserId = string & { __brand: "UserId" }
type OrderId = string & { __brand: "OrderId" }
type Email = string & { __brand: "Email" }
function crearUserId(valor: string): UserId {
return valor as UserId
}
function crearOrderId(valor: string): OrderId {
return valor as OrderId
}
function crearEmail(valor: string): Email {
if (!valor.includes("@")) {
throw new Error("Formato de email inválido")
}
return valor as Email
}
Esta versión es menos robusta que unique symbol porque teóricamente se podría fabricar un objeto con __brand: "UserId", pero en la práctica es suficiente para la mayoría de proyectos.
La elección entre
unique symboly__branddepende del nivel de seguridad requerido. Para la mayoría de aplicaciones,__brandcon tipo literal ofrece un equilibrio adecuado entre simplicidad y protección.
Validación en tiempo de ejecución
Los branded types son más útiles cuando se combinan con validación en tiempo de ejecución. La función constructora verifica que el valor cumpla las restricciones del dominio antes de marcarlo con el brand:
declare const __brand: unique symbol
type Brand<T, B> = T & { readonly [__brand]: B }
type Email = Brand<string, "Email">
type Porcentaje = Brand<number, "Porcentaje">
type CodigoPostal = Brand<string, "CodigoPostal">
function crearEmail(valor: string): Email {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!regex.test(valor)) {
throw new Error(`Email inválido: ${valor}`)
}
return valor as Email
}
function crearPorcentaje(valor: number): Porcentaje {
if (valor < 0 || valor > 100) {
throw new Error(`Porcentaje fuera de rango: ${valor}`)
}
return valor as Porcentaje
}
function crearCodigoPostal(valor: string): CodigoPostal {
if (!/^\d{5}$/.test(valor)) {
throw new Error(`Código postal inválido: ${valor}`)
}
return valor as CodigoPostal
}
const email = crearEmail("usuario@ejemplo.com")
const descuento = crearPorcentaje(15)
const cp = crearCodigoPostal("28001")
console.log(email) // usuario@ejemplo.com
console.log(descuento) // 15
console.log(cp) // 28001
Validación con Result
Para evitar excepciones, se puede combinar la validación con un tipo Result:
declare const __brand: unique symbol
type Brand<T, B> = T & { readonly [__brand]: B }
type Result<T, E = string> =
| { ok: true, valor: T }
| { ok: false, error: E }
type UrlSegura = Brand<string, "UrlSegura">
function crearUrlSegura(valor: string): Result<UrlSegura> {
try {
const url = new URL(valor)
if (url.protocol !== "https:") {
return { ok: false, error: "La URL debe usar HTTPS" }
}
return { ok: true, valor: valor as UrlSegura }
} catch {
return { ok: false, error: "URL con formato inválido" }
}
}
const resultado = crearUrlSegura("https://api.ejemplo.com/datos")
if (resultado.ok) {
console.log(`URL segura: ${resultado.valor}`)
} else {
console.error(resultado.error)
}
Cantidades monetarias con branded types
Un caso de uso clásico es representar cantidades monetarias donde la divisa forma parte del tipo, evitando operaciones entre divisas diferentes:
declare const __brand: unique symbol
type Brand<T, B> = T & { readonly [__brand]: B }
type EUR = Brand<number, "EUR">
type USD = Brand<number, "USD">
type GBP = Brand<number, "GBP">
function eur(cantidad: number): EUR {
return Math.round(cantidad * 100) / 100 as EUR
}
function usd(cantidad: number): USD {
return Math.round(cantidad * 100) / 100 as USD
}
function gbp(cantidad: number): GBP {
return Math.round(cantidad * 100) / 100 as GBP
}
function sumarEUR(a: EUR, b: EUR): EUR {
return ((a as number) + (b as number)) as EUR
}
function sumarUSD(a: USD, b: USD): USD {
return ((a as number) + (b as number)) as USD
}
const precioProducto = eur(29.99)
const impuestos = eur(6.30)
const total = sumarEUR(precioProducto, impuestos)
console.log(`Total: ${total} EUR`) // Total: 36.29 EUR
const precioUSD = usd(35.50)
// sumarEUR(precioProducto, precioUSD) // Error de compilación
Versión genérica con tipo de divisa
declare const __brand: unique symbol
type Divisa = "EUR" | "USD" | "GBP"
type Dinero<D extends Divisa> = number & { readonly [__brand]: D }
function crearDinero<D extends Divisa>(cantidad: number, _divisa: D): Dinero<D> {
return (Math.round(cantidad * 100) / 100) as Dinero<D>
}
function sumar<D extends Divisa>(a: Dinero<D>, b: Dinero<D>): Dinero<D> {
return ((a as number) + (b as number)) as Dinero<D>
}
function restar<D extends Divisa>(a: Dinero<D>, b: Dinero<D>): Dinero<D> {
return ((a as number) - (b as number)) as Dinero<D>
}
function multiplicar<D extends Divisa>(cantidad: Dinero<D>, factor: number): Dinero<D> {
return (Math.round((cantidad as number) * factor * 100) / 100) as Dinero<D>
}
const precio = crearDinero(100, "EUR")
const descuento = crearDinero(15, "EUR")
const precioFinal = restar(precio, descuento)
const conIVA = multiplicar(precioFinal, 1.21)
console.log(`Precio final con IVA: ${conIVA} EUR`)
const precioUS = crearDinero(50, "USD")
// sumar(precio, precioUS) // Error: Dinero<"EUR"> no es Dinero<"USD">
IDs tipados en un sistema de entidades
En aplicaciones con múltiples entidades, los branded types previenen errores al mezclar identificadores:
declare const __brand: unique symbol
type Brand<T, B> = T & { readonly [__brand]: B }
type UserId = Brand<string, "UserId">
type ArticuloId = Brand<string, "ArticuloId">
type ComentarioId = Brand<string, "ComentarioId">
// Funciones constructoras con prefijo para trazabilidad
function crearUserId(valor: string): UserId {
return `usr_${valor}` as UserId
}
function crearArticuloId(valor: string): ArticuloId {
return `art_${valor}` as ArticuloId
}
function crearComentarioId(valor: string): ComentarioId {
return `com_${valor}` as ComentarioId
}
// Interfaces que usan los branded types
interface Articulo {
id: ArticuloId
titulo: string
autorId: UserId
}
interface Comentario {
id: ComentarioId
articuloId: ArticuloId
autorId: UserId
texto: string
}
// Funciones con tipos seguros
function obtenerArticulo(id: ArticuloId): Articulo | null {
console.log(`Buscando articulo: ${id}`)
return null
}
function obtenerComentarios(articuloId: ArticuloId): Comentario[] {
console.log(`Buscando comentarios del articulo: ${articuloId}`)
return []
}
const userId = crearUserId("001")
const articuloId = crearArticuloId("042")
obtenerArticulo(articuloId) // Correcto
obtenerComentarios(articuloId) // Correcto
// obtenerArticulo(userId) // Error de compilación
// obtenerComentarios(userId) // Error de compilación
Type guards para branded types
Los type guards permiten verificar en tiempo de ejecución si un valor puede ser tratado como un branded type:
declare const __brand: unique symbol
type Brand<T, B> = T & { readonly [__brand]: B }
type EnteroPositivo = Brand<number, "EnteroPositivo">
type CadenaNoVacia = Brand<string, "CadenaNoVacia">
function esEnteroPositivo(valor: number): valor is EnteroPositivo {
return Number.isInteger(valor) && valor > 0
}
function esCadenaNoVacia(valor: string): valor is CadenaNoVacia {
return valor.trim().length > 0
}
function procesarCantidad(cantidad: EnteroPositivo): void {
console.log(`Procesando cantidad: ${cantidad}`)
}
function procesarNombre(nombre: CadenaNoVacia): void {
console.log(`Procesando nombre: ${nombre}`)
}
const cantidad = 42
if (esEnteroPositivo(cantidad)) {
procesarCantidad(cantidad) // TypeScript sabe que es EnteroPositivo
}
const nombre = "Ana"
if (esCadenaNoVacia(nombre)) {
procesarNombre(nombre) // TypeScript sabe que es CadenaNoVacia
}
Función auxiliar genérica
declare const __brand: unique symbol
type Brand<T, B> = T & { readonly [__brand]: B }
type Validador<T> = (valor: T) => boolean
function crearConstructor<T, B extends string>(
validar: Validador<T>
) {
return function (valor: T): Brand<T, B> {
if (!validar(valor)) {
throw new Error(`Validación fallida para el valor: ${valor}`)
}
return valor as Brand<T, B>
}
}
type Latitud = Brand<number, "Latitud">
type Longitud = Brand<number, "Longitud">
const crearLatitud = crearConstructor<number, "Latitud">(
(v) => v >= -90 && v <= 90
)
const crearLongitud = crearConstructor<number, "Longitud">(
(v) => v >= -180 && v <= 180
)
interface Coordenadas {
lat: Latitud
lon: Longitud
}
function crearCoordenadas(lat: number, lon: number): Coordenadas {
return {
lat: crearLatitud(lat),
lon: crearLongitud(lon)
}
}
const madrid = crearCoordenadas(40.4168, -3.7038)
console.log(`Madrid: ${madrid.lat}, ${madrid.lon}`)
try {
crearCoordenadas(200, -3.7038) // Error: latitud fuera de rango
} catch (error) {
console.error((error as Error).message)
}
Los branded types son una técnica fundamental en TypeScript para proyectos donde la corrección del dominio es crítica. Combinados con funciones constructoras que validan en tiempo de ejecución, proporcionan una capa de seguridad que el tipado estructural por si solo no puede ofrecer.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender el problema del tipado estructural con tipos semánticamente distintos. Crear branded types usando unique symbol y __brand. Aplicar branded types a IDs, cadenas validadas y cantidades monetarias. Combinar validación en tiempo de ejecución con branded types.