Inicio / Artículos / Campos privados en TypeScript frente a la sintaxis #: Privacidad en tiempo de compilación o en tiempo de ejecución

Campos privados en TypeScript frente a la sintaxis #: Privacidad en tiempo de compilación o en tiempo de ejecución

Compara el modificador `private` en tiempo de compilación de TypeScript con los campos `#` aplicados en tiempo de ejecución según ECMAScript para ayudarte a elegir la estrategia de encapsulamiento adecuada para los conjuntos de código de 2026.

2729 palabras

La mayoría de los defectos relacionados con la privacidad en los proyectos TypeScript se deben a confundir dos modelos de encapsulamiento que en realidad no funcionan de la misma manera: la palabra clave private que solo funciona en el compilador y la sintaxis de campo # impuesta en tiempo de ejecución según ECMAScript. Los equipos a menudo eligen un enfoque sin pensar demasiado, lo implementan y más tarde se encuentran con situaciones en las que esa elección falla de maneras inesperadas.

La palabra clave private de TypeScript no brinda protección una vez que el código se ejecuta. El compilador verifica la visibilidad mientras escribes y construyes el proyecto, pero el JavaScript que genera convierte cada miembro “private” en una propiedad pública ordinaria. Cualquier cosa que utilice la salida compilada puede ignorar por completo el modificador de acceso.

Los campos # de ECMAScript adoptan un enfoque diferente que ofrece una privacidad real. El propio entorno de ejecución impone este límite al almacenar los datos de los campos en un WeakMap interno, por lo que el código externo no puede acceder a ellos. Esa garantía te protege contra un uso accidental y mantiene seguros los valores sensibles incluso en entornos en los que no confías plenamente.

Elegir entre estos dos enfoques determina si tu encapsulación realmente resistirá una vez que el código llegue a producción, y por eso es tan importante acertar en esta decisión.

Puntos clave

  • El modificador private de TypeScript se elimina durante la compilación, dejando propiedades de JavaScript simples y accesibles libremente. En contraste, los campos # de ECMAScript utilizan un almacenamiento basado en WeakMap que mantiene la privacidad intacta incluso después de la transpilación.
  • Utilice private cuando necesite seguridad a nivel de tipo dentro de un código donde TypeScript es el único consumidor y los controles en tiempo de compilación sean suficientes. Opte por # cuando esté publicando una biblioteca, manejando importaciones dinámicas o protegiendo valores sensibles de inspecciones en tiempo de ejecución.
  • Estos dos mecanismos no son sustitutos directos el uno del otro. Cambiar una clase de private a # modifica la apariencia de la API pública y puede dañar herramientas que dependen de la reflexión. Sin una convención documentada, los códigos terminan combinando ambos enfoques de manera inconsistente.
  • Muchos equipos recurren a private simplemente porque se parece a patrones de lenguajes como Java, y luego se sorprenden cuando los consumidores que solo usan JavaScript ignoran por completo dicho contrato. Los errores resultantes suelen ser sutiles pero difíciles de detectar.
  • A partir de 2026, TypeScript 5.7 y versiones posteriores admiten ambos mecanismos con inferencia de tipos completa. Elegir entre ellos se reduce realmente a cuestiones de confianza: ¿controlas cada fragmento de código que interactúa con tu clase, o podría ejecutarse en un entorno hostil?
  • Comprendiendo el modificador private de TypeScript: solo en tiempo de compilación

    La palabra clave private en TypeScript es puramente una construcción del sistema de tipos. Bloquea el acceso no autorizado durante el desarrollo, pero una vez compilado, el JavaScript resultante contiene propiedades normales y sin protección. En la práctica, las funciones private funcionan más como una ayuda para la documentación que como un verdadero límite de seguridad.

    Cuando una clase con un campo private se compila hasta convertirse en JavaScript puro, el modificador desaparece por completo y el campo se transforma en una propiedad ordinaria que cualquier usuario puede leer u sobrescribir directamente. Esto se convierte en un problema real en tres situaciones: al enviar paquetes a npm, al importar dinámicamente módulos de terceros o al integrarse con herramientas basadas en reflexión como serializadores y ORMs.

    La simplicidad es el principal punto a favor de private. Los desarrolladores provenientes de lenguajes como Java o C# lo comprenden de inmediato. Los editores ocultan los miembros privados de las sugerencias de autocompletado, las herramientas de refactorización respetan la visibilidad pretendida y el compilador señala cualquier exposición accidental durante el desarrollo y las revisiones de código.

    Los problemas comienzan cuando las suposiciones sobre el entorno de ejecución resultan ser incorrectas. Imagine un equipo que crea un panel de control interno bajo la suposición de que todos los usuarios de su código ejecutan TypeScript. Meses después, un servicio escrito en Python carga el paquete de JavaScript compilado y comienza a modificar directamente los tokens de sesión. El supuesto límite de privacidad nunca existió fuera del compilador, por lo que no ofrecía ninguna resistencia.

    Este modelo funciona bien siempre y cuando controle toda la cadena de dependencias y se aplique TypeScript de extremo a extremo. Tan pronto como su código cruza una frontera lingüística o se publica en algún lugar público, private deja de ser una garantía y se convierte en poco más que una sugerencia.

    Campos privados de ECMAScript (#): Privacidad estricta garantizada en tiempo de ejecución

    Los campos precedidos por # son una característica nativa de JavaScript que crea propiedades realmente inaccesibles desde el exterior de la clase. El motor las mantiene en un WeakMap interno, ocultas tanto de la reflexión como de cualquier código externo que intente acceder a ellas. Cuando se utiliza ES2022 o una versión más reciente, TypeScript mantiene intacta la sintaxis # en su salida.

    Al compilar para objetivos modernos, el JavaScript generado conserva la sintaxis # tal como está, en lugar de convertirla en una propiedad normal. La encapsulación aquí está garantizada por el propio entorno de ejecución: intentar acceder a un campo como wallet.#balance desde fuera de la clase genera un error de sintaxis en modo estricto. Incluso llamando a Object.keys(wallet) se obtiene un array vacío, ya que los campos privados están completamente fuera del mecanismo normal de enumeración de propiedades.

    El compromiso con los campos # es la compatibilidad. Al dirigirse a entornos antiguos como ES5 o ES2015, TypeScript se ve obligado a generar polyfills basados en WeakMap, lo que aumenta el tamaño del paquete y genera un costo adicional por cada acceso; esto representa un problema real para las bibliotecas destinadas a entornos de navegador donde la velocidad es crucial.

    También existe un costo en cuanto a la experiencia del desarrollador. Los editores no pueden ofrecer completado automático para los campos # desde fuera de su clase, y algunas herramientas de depuración los ocultan por defecto en los inspeccionadores de objetos. Las utilidades de serialización como JSON.stringify también omiten silenciosamente los campos privados, lo que puede sorprender a los desarrolladores que esperan un registro completo del estado de un objeto.

    Los campos privados tienen más sentido en tres casos: proteger claves criptográficas o tokens de acceso, evitar que los consumidores de API modifiquen invariantes internos, y ejecutar código en entornos donde no se puede confiar plenamente en lo que se está ejecutando junto a él. En estos escenarios, la sólida garantía en tiempo de ejecución vale más que la comodidad que se sacrifica.

    Comparación lado a lado: cuándo gana cada enfoque

    Elegir entre private y # depende en última instancia de los límites de confianza y las necesidades de herramientas del equipo; ninguno es la opción por defecto correcta en todas las situaciones. Esto significa que los equipos deben acordar reglas explícitas en lugar de optar simplemente por lo que les resulte más familiar.

    Utilice private de TypeScript cuando:

    • Está desarrollando una aplicación interna donde cada consumidor es código en TypeScript con configuraciones estrictas del compilador.
  • Depende de ORM, serializadores u otras herramientas basadas en reflexión que necesitan enumerar propiedades para crear esquemas de base de datos o cargas de API.
  • Está compilando para entornos antiguos como ES5, donde los polyfills de campos # aumentarían excesivamente el tamaño del paquete.
  • Para esa clase en particular, la experiencia del desarrollador y el autocompletado son más importantes que la seguridad a nivel de tiempo de ejecución.
  • Utilice los campos # de ECMAScript cuando:

    • Está publicando un paquete en npm y no puede garantizar que todos los usuarios respeten las convenciones exclusivas de TypeScript.
    • Está almacenando valores sensibles como tokens de autenticación, claves de cifrado o información de pago que no deben ser inspeccionables.
    • Está creando un sistema de plugins donde código de terceros no confiable comparte el mismo tiempo de ejecución que el suyo.
    • Estás diseñando un framework o SDK en el que el contrato de la API debe ser respetado estructuralmente, y no solo documentado.

    Surgen conflictos cuando un diseño necesita tanto soporte de reflexión como verdadera privacidad en tiempo de ejecución al mismo tiempo. Un caso típico: un ORM intenta enumerar cada campo para crear una correspondencia con la base de datos, pero los campos # simplemente no aparecen en esa enumeración. Para solucionarlo, generalmente se deben añadir métodos getter explícitos o decoradores de metadatos, lo cual agrega complejidad que muchos equipos prefieren evitar.

    Las pruebas introducen otro problema. Con el modificador private de TypeScript, los archivos de prueba del mismo proyecto aún pueden acceder al estado interno mediante afirmaciones de tipo. Con los campos #, por lo general hay que extraer el comportamiento susceptible de prueba en métodos separados o recurrir a la inyección de dependencias. Los equipos acostumbrados a manipular componentes internos privados durante las pruebas suelen encontrar este obstáculo adicional molesto.

    Si observamos la situación en 2026, los campos # son cada vez más comunes en bibliotecas sensibles a la seguridad, mientras que los modificadores private siguen siendo la norma en el código interno de las aplicaciones. TypeScript 5.7 trata ambos como características de primera clase plenamente soportadas, con inferencia y verificación de errores, por lo que la decisión se basa realmente en la arquitectura y no en las capacidades del compilador.

    Código en el mundo real: Implementación de ambos patrones

    Es común que un único código de producción utilice ambos patrones para diferentes propósitos al mismo tiempo. Lo importante es mantener la consistencia dentro de un límite determinado: usar private para los detalles normales de implementación y reservar # para los campos relacionados con la seguridad.

    Considere una clase que contiene un campo requestCache marcado como private, junto con un campo #authToken marcado como hard-private. El requestCache se mantiene como private porque las herramientas de prueba y depuración se benefician al poder verlo, y las herramientas de serialización pueden acceder a él si eso resulta útil en algún momento. Mientras tanto, #authToken tiene un nivel de privacidad elevado, ya que exponerlo en tiempo de ejecución crearía un verdadero riesgo de seguridad.

    Tener ambos enfoques tiene sentido siempre que los campos presenten diferentes niveles de riesgo. Elementos como los valores de configuración y las cachés son detalles internos para los cuales cierta flexibilidad es aceptable. Por otro lado, las credenciales y las claves criptográficas requieren una garantía de ejecución más estricta.

    Otra ilustración práctica es una máquina de estados que mantiene sus reglas de transición como private, pero oculta su estado actual detrás de #. La máquina de estados expone el estado únicamente a través del método getState(), manteniendo el campo subyacente #currentState fuera del alcance, lo que impide que el código externo lo modifique directamente. El mapa validTransitions permanece como un campo private porque los conjuntos de pruebas aún pueden necesitar inspeccionar el conjunto de reglas para verificar casos límite.

    Esta convención funciona razonablemente bien. Una base de código con, digamos, 50 clases podría utilizar campos # en unas 10 clases relacionadas con la autenticación, mientras que en todo lo demás se recurriría a private. Ver # en el código sirve entonces como una señal clara de que se ha cruzado un límite sensible desde el punto de vista de la seguridad.

    Estrategias de migración y convenciones del equipo

    Cambiar una clase de private a # supone un cambio disruptivo en lo que respecta a su interfaz pública. Cualquier herramienta que dependa de la enumeración de propiedades dejará de funcionar correctamente, por lo que este tipo de migración debe planificarse cuidadosamente y implementarse gradualmente, con coordinación entre los equipos que dependan del código afectado.

    La migración de una clase de private a campos # suele seguir una secuencia predecible:

    1. Revisar qué campos realmente requieren protección de privacidad aplicada en tiempo de ejecución y cuáles solo necesitan verificaciones de visibilidad a nivel del compilador.
    2. Posponer el lanzamiento de una versión importante que documente los cambios en la interfaz pública de la API.
    3. Convertir los campos críticos desde el punto de vista de la seguridad a sintaxis # dentro de una rama de características dedicada.
    4. Volver a diseñar las pruebas internas para que ya no dependan de acceder directamente a los campos.
    5. Confirmar que las bibliotecas de serialización y los ORMs sigan funcionando correctamente con la estructura actualizada de los campos.
    6. Publicar el lanzamiento con notas detalladas que expliquen exactamente qué falló y por qué.

    Los repositorios de código internos tienen un camino más sencillo en este caso. Dado que no hay consumidores externos de los que preocuparse, los equipos pueden implementar los cambios de forma gradual sin necesidad de seguir un sistema de versionado semántico. La verdadera dificultad radica en la coordinación: los desarrolladores necesitan un entendimiento común sobre cuándo # es obligatorio y cuándo private sigue siendo aceptable.

    Una convención práctica que adoptan muchos equipos es la siguiente:

    • Reserve # para tokens de autenticación, claves de cifrado, credenciales de bases de datos e información de identificación personal.
    • Use private para capas de caché, estado de configuración, máquinas de estado internas y valores derivados o calculados.
    • Incluya las razones detrás de estas reglas en la lista de verificación de revisión de código y en los documentos de incorporación para que los nuevos empleados las comprendan rápidamente.
  • Añadir reglas de lint que detecten patrones riesgosos, como un campo de contraseña declarado con private en lugar de #.
  • Las organizaciones que mantienen tanto TypeScript como JavaScript heredado en el mismo repositorio necesitan un conjunto de reglas ligeramente diferente. En un monorepo que combina servicios de TypeScript con módulos de JavaScript más antiguos, aplicar campos # en todas partes no es práctico debido al costo de transpilación que implica para el código que no lo necesita. En ese escenario, la línea divisoria suele estar a nivel de repositorio o paquete: los módulos de TypeScript escritos recientemente adoptan # para datos sensibles, mientras que el JavaScript heredado permanece sin cambios hasta que finalmente se vuelva a escribir.

    Una estrategia híbrida de este tipo se observa en los sistemas distribuidos que combinan privacidad en tiempo de ejecución estricta en algunos servicios con contratos a nivel de tipo en otros, donde ciertos componentes imponen un encapsulamiento estricto mientras que otros dependen exclusivamente de las verificaciones del compilador.

    La mayoría de los problemas de migración se deben a tratar el conmutador como un mecanismo puramente mecánico de búsqueda y reemplazo. Sustituir private por # sin auditar primero cada consumidor de ese campo conduce a fallos silenciosos. Considere una herramienta de registro basada en reflexión que recorre las propiedades de un objeto para generar información de depuración: una vez que esas propiedades se convierten en campos #, desaparecen de esa enumeración y el registrador pierde silenciosamente la capacidad de ver el estado del objeto. La solución adecuada es exponer los datos necesarios a través de métodos getter explícitos o una interfaz estructurada para el registro, en lugar de depender de la reflexión para acceder a los componentes internos privados.

    Preguntas frecuentes

    ¿Puedo mezclar modificadores private y campos # en la misma clase?

    Sí. TypeScript 5.7 y versiones posteriores permiten que ambos mecanismos coexistan dentro de una misma definición de clase. Un patrón común es aplicar private a los detalles de implementación a los que las pruebas o herramientas de depuración aún puedan necesitar acceder, mientras se reserva # para los pocos campos que deben resistir cualquier tipo de inspección en tiempo de ejecución. El compilador los trata como dos sistemas de visibilidad separados pero compatibles.

    ¿Funcionan los campos # en entornos de JavaScript más antiguos como IE11?

    No directamente. Cuando el objetivo de compilación es ES5 o ES2015, el compilador de TypeScript recurre a la emisión de polyfills basados en WeakMap para simular el comportamiento de los campos #, lo que aumenta el tamaño del código y genera un costo en rendimiento en tiempo de ejecución. Si aún necesita soportar navegadores antiguos, es mejor seguir utilizando los modificadores private y aceptar la verificación únicamente en tiempo de compilación, en lugar de asumir las desventajas derivadas de los polyfills.

    ¿Qué sucede con los campos # durante la serialización a JSON?

    JSON.stringify y las herramientas de serialización similares simplemente no pueden detectar los campos #; por lo tanto, cualquier objeto que los contenga se serializará sin esas propiedades en absoluto. Si necesita que parte de ese estado privado se muestre en el resultado, debe exponerlo intencionadamente, ya sea a través de un método getter o definiendo una implementación personalizada de toJSON que controle con precisión qué se incluirá.

    ¿Pueden las subclases acceder a los campos # de la clase padre?

    No. Los campos privados de ECMAScript pertenecen exclusivamente a la clase en la que se declaran, y ese límite es absoluto: una subclase no puede leer ni modificar los campos # de la clase padre, ni siquiera de forma indirecta a través de métodos protegidos o públicos. Esta es una diferencia significativa con respecto a los modificadores private, ya que el compilador de TypeScript a veces permite el acceso desde subclases mediante afirmaciones explícitas de tipo.

    ¿Debería migrar bases de código existentes de campos private a campos #?

    Solo cuando existe una necesidad concreta: una exposición real de seguridad o un requisito genuino de privacidad que deba aplicarse en tiempo de ejecución. Dado que la migración constituye un cambio disruptivo que afecta a las herramientas, al comportamiento de serialización y al diseño de pruebas, no es algo que se deba realizar a la ligera. Para la mayoría de las aplicaciones internas, los modificadores private ya ofrecen un encapsulamiento adecuado, y el costo de la migración no merece la pena. Priorice primero la migración para bibliotecas compartidas, APIs orientadas al público o módulos que manejan datos sensibles.

    Elegir el modelo de privacidad adecuado para su código en 2026

    Elegir entre los campos private de TypeScript y # de ECMAScript no es cuestión de preferencia arbitraria. La elección determina si la garantía de encapsulamiento se mantiene más allá del compilador, afecta la forma en que el código externo puede interactuar con tus clases y comunica la intención arquitectónica a quien mantenga el código en el futuro.

    Utiliza private cuando controlas completamente el grafo de dependencias y priorizas las herramientas para desarrolladores sobre garantías estrictas en tiempo de ejecución. Opta por # cuando tu código se ejecuta en entornos no confiables o maneja datos que nunca deben exponerse mediante reflexión. La mayoría de los conjuntos de código del mundo real terminan necesitando una combinación de ambos, aplicada de manera deliberada según lo que represente cada campo.

    Tomar una decisión errónea en este sentido suele tener consecuencias en la producción: las invariantes se ven afectadas por mutaciones accidentales, las credenciales se filtran a través de la infraestructura de registro de eventos, o los conjuntos de pruebas terminan sin poder verificar en absoluto el estado interno. Todo esto se puede evitar con convenciones claras y reglas arquitectónicas documentadas.

    Los equipos que desarrollan herramientas internas pueden confiar generalmente en los modificadores private y aprovechar las herramientas ya consolidadas que existen alrededor de ellos. Los equipos que publican bibliotecas o operan en dominios sensibles desde el punto de vista de la seguridad necesitan las garantías en tiempo de ejecución que ofrecen los campos #. Ambos enfoques cuentan con el mismo nivel de soporte en el ecosistema actual de TypeScript; la elección adecuada depende de su modelo de amenazas y del grado de confianza que tenga en los usuarios de su API.

    Lecturas relacionadas

  • El ascenso de TypeScript: ¿Por qué las equipos pasan del JavaScript simple? — Explore las razones técnicas, desde la seguridad de tipos hasta las herramientas y los flujos de trabajo con IA, que impulsan a los equipos a adoptar TypeScript en lugar del JavaScript simple en el desarrollo moderno.