Branded types en TypeScript

Avanzado
TypeScript
TypeScript
Actualizado: 18/04/2026

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.

Branded types para tipado nominal

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 symbol y __brand depende del nivel de seguridad requerido. Para la mayoría de aplicaciones, __brand con 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 - Autor del tutorial

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.