Saltar al contenido
webtype.orgwebtype.org

    ↑↓ mover · ⏎ abrir · esc cerrar

    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

    1. 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.

    2. 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.

    3. 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

    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.

    Véase también

    Patrones