Patrones
¿Cómo evito que un UserId se pase donde se esperaba un OrderId, si ambos son solo cadenas?
TypeScript es estructural, así que dos alias de string son el mismo tipo. Una propiedad fantasma los hace distintos sin cambiar el valor.
La receta
declare const brand: unique symbol type Brand<T, Name extends string> = T & { readonly [brand]: Name } type UserId = Brand<string, 'UserId'> type OrderId = Brand<string, 'OrderId'> const asUserId = (raw: string): UserId => raw as UserId declare function findUser(id: UserId): void const id = asUserId('u_1') findUser(id)
La compilación compila esto y comprueba cada resultado de abajo.
Cómo funciona
- 01
declare const brand: unique symbolUn
unique symbolda una clave que nada más puede producir. Una clave de cadena permitiría que un objeto ajeno satisficiera la marca por accidente. - 02
type Brand<T, Name extends string> = T & { readonly [brand]: Name }La intersección añade una propiedad que solo existe en el tipo. En tiempo de ejecución el valor sigue siendo exactamente la cadena de partida.
- 03
const asUserId = (raw: string): UserId => raw as UserIdUn cast, en un solo sitio, es el precio. En todo lo demás el compilador impone la distinción gratis — por eso esta función debería ser la única entrada.
Lo que obtienes
UserId extends string ? true : false→truestring extends UserId ? true : false→falseLa asimetría es toda la característica: un id marcado sigue siendo una cadena, pero una cadena a secas no es un id marcado.
UserId extends OrderId ? true : false→false
Dónde falla
La marca se borra, así que JSON.parse te dará sin problema algo tipado como UserId que nunca pasó por tu constructor. Las marcas protegen tu código de sí mismo; no validan datos que cruzan una frontera.
Conclusión
Usa una marca cuando dos valores comparten representación y no deben compartir significado. Mantén exactamente una función que produzca cada uno.

