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 symbol
Un `unique symbol` da 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 UserId
Un 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.