Champs privés de TypeScript contre syntaxe # : confidentialité en temps de compilation ou en temps d’exécution
Compare le modificateur `private` en temps de compilation de TypeScript avec les champs `#` appliqués en temps d’exécution par ECMAScript afin de vous aider à choisir la stratégie d’encapsulation appropriée pour les bases de code de 2026.
La plupart des défauts liés à la confidentialité dans les projets TypeScript proviennent de la confusion entre deux modèles d’encapsulation qui ne fonctionnent pas de la même manière : le mot-clé private réservé au compilateur et la syntaxe de champ # imposée en temps de exécution selon ECMAScript. Les équipes choisissent souvent une approche sans y réfléchir davantage, la mettent en œuvre, puis se retrouvent confrontées à des situations où ce choix échoue de manières inattendues.
Le mot-clé private de TypeScript ne fournit aucune protection une fois que votre code est en exécution. Le compilateur vérifie la visibilité pendant que vous écrivez et compilez le projet, mais le JavaScript généré transforme chaque membre « private » en une propriété publique ordinaire. Tout ce qui consomme le résultat compilé peut donc contourner complètement les modificateurs d’accès.
Les champs # d’ECMAScript adoptent une approche différente, assurant une véritable confidentialité. Le moteur de exécution impose cette limite en stockant les données des champs dans un WeakMap interne, de sorte que le code externe ne peut pas y accéder. Cette garantie vous protège contre une utilisation accidentelle et maintient les valeurs sensibles en sécurité, même dans des environnements auxquels vous ne faites pas entièrement confiance.
Le choix entre ces deux approches détermine si votre encapsulation résistera réellement une fois le code mis en production, c’est pourquoi il est si important de bien choisir.
Points clés
- Le modificateur
privatede TypeScript est supprimé lors de la compilation, ne laissant que des propriétés JavaScript ordinaires et librement accessibles. En revanche, les champs#d’ECMAScript utilisent un stockage basé sur WeakMap qui préserve la confidentialité même après la transpilation.
private lorsque vous avez besoin de sécurité au niveau du type dans un codebase où TypeScript est le seul consommateur et où des vérifications en temps de compilation suffisent. Optez pour # lorsque vous publiez une bibliothèque, que vous travaillez avec des imports dynamiques ou que vous souhaitez protéger des valeurs sensibles contre toute inspection en temps d’exécution.private en # modifie l’API publique et peut endommager des outils qui reposent sur la réflexion. En l’absence d’une convention documentée, les codebases finissent par mélanger ces deux approches de manière incohérente.private simplement parce que cela ressemble à des patterns utilisés dans des langages comme Java, puis sont prises au dépourvu lorsque des consommateurs basés uniquement sur JavaScript ignorent complètement ce contrat. Les bugs qui en résultent sont souvent discrets mais difficiles à détecter.Comprendre le modificateur private de TypeScript : uniquement en temps de compilation
Le mot-clé private dans TypeScript n’est qu’une construction du système de types. Il bloque l’accès non autorisé pendant le développement, mais une fois compilé, le JavaScript résultant contient des propriétés ordinaires sans protection. En pratique, les fonctions private servent davantage d’aide à la documentation que de véritable barrière de sécurité.
Lorsqu’une classe possédant un champ private est compilée en JavaScript pur, le modificateur disparaît complètement, et le champ devient une propriété ordinaire que n’importe quel utilisateur peut lire ou écrire directement. Cela pose un véritable problème dans trois situations : l’envoi de paquets vers npm, l’import dynamique de modules tiers, ou l’intégration avec des outils basés sur la réflexion tels que des sérialiseurs et des ORM.
La simplicité est le principal avantage du private. Les développeurs venant de langages comme Java ou C# l’adoptent immédiatement. Les éditeurs cachent les membres privés des suggestions d’autocomplétion, les outils de refactoring respectent la visibilité prévue, et le compilateur signale toute exposition accidentelle pendant le développement et les revues de code.
Les problèmes commencent lorsque les hypothèses concernant le temps d’exécution s’avèrent fausses. Imaginez une équipe qui crée un tableau de bord interne en partant du principe que tous les utilisateurs de son code exécutent TypeScript. Des mois plus tard, un service écrit en Python charge le bundle JavaScript compilé et commence à modifier directement les tokens de session. La frontière de confidentialité supposée n’existait pas réellement en dehors du compilateur, ce qui signifie qu’elle ne présentait aucune résistance.
Cet modèle fonctionne bien tant que vous contrôlez toute la chaîne de dépendances et que TypeScript est appliqué du début à la fin. Dès que votre code dépasse une frontière linguistique ou est publié quelque part en ligne, le statut private cesse d’être une garantie pour devenir peu plus qu’une suggestion.
Champs privés d’ECMAScript (#) : confidentialité stricte garantie par le temps d’exécution
Les champs préfixés de # constituent une fonctionnalité native de JavaScript qui crée des propriétés véritablement inaccessibles depuis l’extérieur de la classe. Le moteur les conserve dans un WeakMap interne, caché des mécanismes de réflexion ainsi que de tout code externe tentant d’y accéder. Lorsque vous ciblez ES2022 ou une version ultérieure, TypeScript conserve la syntaxe # telle quelle dans son output.
Lors de la compilation pour des cibles modernes, le JavaScript généré préserve la syntaxe # sans la convertir en une propriété normale. L’encapsulation est ici garantie par le runtime lui-même : toute tentative d’accéder à un champ tel que wallet.#balance depuis l’extérieur de la classe provoque une erreur de syntaxe en mode strict. Même l’appel à Object.keys(wallet) renvoie un tableau vide, car les champs privés échappent complètement au mécanisme d’énumération des propriétés normal.
Le compromis lié aux champs # concerne la compatibilité. Cibler des environnements plus anciens tels que ES5 ou ES2015 force TypeScript à générer des polyfills basés sur WeakMap, ce qui augmente la taille du bundle et entraîne des coûts supplémentaires à chaque accès — un problème réel pour les bibliothèques destinées à des environnements de navigateur exigeants en termes de performance.
Il existe également un impact sur l’expérience des développeurs. Les éditeurs ne peuvent pas proposer de complétion automatique pour les champs # en dehors de leur classe, et certains outils de débogage les cachent par défaut dans les inspecteurs d’objets. Les outils de sérialisation tels que JSON.stringify omettent également silencieusement les champs privés, ce qui peut surprendre les développeurs qui s’attendent à une vue complète de l’état d’un objet.
Les champs privés sont les plus pertinents dans trois cas : pour protéger les clés cryptographiques ou les tokens d’accès, pour empêcher les utilisateurs de l’API de modifier des invariants internes, et pour exécuter du code dans des environnements où l’on ne peut pas faire entièrement confiance à ce qui s’exécute en même temps. Dans ces situations, la forte garantie de temps d’exécution vaut plus que la commodité que l’on sacrifie.
Comparaison côte à côte : quand chaque approche l’emporte
Le choix entre private et # dépend finalement de vos limites de confiance et de vos besoins en outils — aucun des deux n’est la valeur par défaut appropriée dans toutes les situations. Cela signifie que les équipes doivent convenir de règles explicites plutôt que de choisir simplement ce qui leur semble le plus familier.
Préférez private de TypeScript lorsque :
- Vous développez une application interne où chaque utilisateur est du code TypeScript sous des paramètres de compilateur stricts.
# augmenteraient excessivement la taille du bundle.Utilisez les champs ECMAScript # lorsque :
- Vous publiez un paquet sur npm et ne pouvez pas garantir que tous les utilisateurs respecteront uniquement les conventions de TypeScript.
- Vous stockez des valeurs sensibles telles que des tokens d’authentification, des clés de chiffrement ou des informations de paiement qui ne doivent pas être consultables.
- Vous développez un système de plugins où du code tiers non fiable partage le même runtime que le vôtre.
- Vous concevez un framework ou un SDK où le contrat API doit être respecté structurellement, et non seulement documenté.
Des conflits surviennent lorsque une conception nécessite à la fois un support de réflexion et une véritable confidentialité en temps de exécution. Un cas typique : un ORM tente d’énumérer tous les champs pour créer une correspondance avec la base de données, mais les champs # ne figurent tout simplement pas dans cette énumération. Pour y remédier, il faut généralement ajouter des méthodes getter explicites ou des décorateurs de métadonnées, ce qui augmente la complexité que de nombreuses équipes préféreraient éviter.
Les tests introduisent un autre problème. Avec le modificateur private de TypeScript, les fichiers de test du même projet peuvent encore accéder à l’état interne via des assertions de type. Avec les champs #, il faut généralement extraire le comportement testable dans des méthodes séparées ou recourir à l’injection de dépendances. Les équipes habituées à manipuler les éléments internes privés lors des tests trouvent souvent cette contrainte supplémentaire ennuyeuse.
Au vu de la situation en 2026, les champs # deviennent de plus en plus courants dans les bibliothèques sensibles à la sécurité, tandis que les modificateurs private restent la norme pour le code d’application interne. TypeScript 5.7 prend en charge pleinement les deux en tant que fonctionnalités de première classe, avec inférence et vérification des erreurs ; par conséquent, le choix dépend davantage de l’architecture que des capacités du compilateur.
Code en situation réelle : mise en œuvre des deux patterns
Il est courant qu’une même base de code de production utilise les deux modèles en même temps à des fins différentes. L’important est de rester cohérent dans un cadre donné : utiliser private pour les détails d’implémentation ordinaires, et réserver # aux champs liés à la sécurité.
Prenons l’exemple d’une classe qui contient un champ requestCache marqué comme private, ainsi qu’un champ #authToken marqué comme étant hautement privé. Le champ requestCache reste private car les outils de test et de débogage peuvent en tirer profit, et les outils de sérialisation peuvent l’intercepter si cela s’avère utile. En revanche, #authToken est considéré comme hautement privé, car son exposition en temps de exécution créerait réellement une faille de sécurité.
Mélanger les deux est judicieux lorsque les champs présentent des niveaux de risque différents. Des éléments tels que les valeurs de configuration et les caches sont des détails internes pour lesquels une certaine flexibilité est acceptable. En revanche, les identifiants d’accès et les clés cryptographiques nécessitent une garantie de fonctionnement plus stricte.
Un autre exemple concret est une machine à états qui conserve ses règles de transition en tant que champ private, mais cache son état réel derrière un symbole #. La machine à états ne expose l’état qu’à travers une méthode getState(), tout en gardant le champ sous-jacent #currentState inaccessible, empêchant ainsi tout code externe de le modifier directement. La table de correspondance validTransitions reste un champ private, car les suites de tests peuvent encore avoir besoin d’examiner cet ensemble de règles pour vérifier des cas limites.
Cette convention s’adapte assez bien. Une base de code comptant, disons, 50 classes pourrait utiliser des champs # dans une dizaine de classes liées à l’authentification, tout en recourant à private partout ailleurs. La présence de # dans le code sert alors comme un signal clair indiquant que l’on est entré dans une zone sensible du point de vue de la sécurité.
Stratégies de migration et conventions d’équipe
Passer d’une classe private à des champs # représente un changement disruptif pour ses interfaces publiques. Tout outil qui dépend de l’énumération des propriétés cessera de fonctionner correctement, ce qui exige que ce type de migration soit planifié soigneusement et mis en œuvre progressivement, avec une coordination entre toutes les équipes qui utilisent le code concerné.
La migration d’une classe de private vers des champs # suit généralement une séquence prévisible :
- Évaluer quels champs nécessitent réellement une protection de la confidentialité appliquée en temps de exécution, par rapport aux champs qui ne requièrent que des vérifications de visibilité au niveau du compilateur.
- Annuler une version majeure qui documentait les modifications apportées à l’API publique.
- Convertir les champs critiques pour la sécurité en syntaxe
#au sein d’une branche fonctionnelle dédiée. - Réécrire les tests internes afin qu’ils ne dépendent plus d’un accès direct aux champs.
- Vérifier que les bibliothèques de sérialisation et les ORM continuent de fonctionner correctement avec la nouvelle disposition des champs.
- Publier la version avec des notes détaillées expliquant précisément ce qui a cessé de fonctionner et pourquoi.
Les bases de code internes ont un chemin plus simple à suivre dans ce cas. Comme il n’y a pas de consommateur externe à prendre en compte, les équipes peuvent déployer les modifications progressivement sans avoir à gérer une versionnement sémantique. La véritable difficulté réside alors dans la coordination : les développeurs doivent partager une compréhension commune quant au moment où # est obligatoire et quand private reste acceptable.
Une convention pratique adoptée par de nombreuses équipes est la suivante :
- Réservez
#aux tokens d’authentification, aux clés de chiffrement, aux identifiants de base de données et aux informations permettant d’identifier une personne. - Utilisez
privatepour les couches de mise en cache, l’état de configuration, les machines à états internes ainsi que pour les valeurs dérivées ou calculées. - Incluez les raisons de ces choix dans la liste de contrôle des revues de code et dans les documents d’intégration afin que les nouveaux employés puissent assimiler rapidement ces règles.
private au lieu de #.Les organisations qui gèrent à la fois TypeScript et du JavaScript ancien dans le même répertoire ont besoin d’un ensemble de règles légèrement différent. Dans un monorepo combinant des services TypeScript avec des modules JavaScript plus anciens, l’application de champs # partout n’est pas pratique en raison du coût de transpilation qu’elle entraîne pour des codes qui n’en ont pas besoin. Dans ce type de configuration, la ligne de séparation se situe généralement au niveau du répertoire ou du package : les modules TypeScript nouvellement écrits utilisent # pour les données sensibles, tandis que le JavaScript ancien reste inchangé jusqu’à ce qu’il soit finalement réécrit.
La plupart des problèmes de migration découlent du fait que l’on traite le compilateur comme un outil purement mécanique de recherche et de remplacement. Remplacer private par # sans d’abord auditer chaque utilisation de ce champ entraîne des dysfonctionnements silencieux. Prenons l’exemple d’une utilité de journalisation basée sur la réflexion qui parcourt les propriétés d’un objet pour générer des informations de débogage : une fois ces propriétés devenues des champs #, elles disparaissent de cette énumération et le journaliseur perd silencieusement la capacité d’observer l’état de l’objet. La solution appropriée consiste à exposer les données nécessaires via des méthodes getter explicites ou une interface de journalisation structurée, plutôt que de compter sur la réflexion pour accéder aux éléments internes privés.
Questions fréquentes
Puis-je mélanger des modificateurs private et des champs # dans la même classe ?
Oui. TypeScript 5.7 et versions ultérieures permettent à ces deux mécanismes de coexister au sein d’une même définition de classe. Un schéma courant consiste à utiliser private pour les détails d’implémentation auxquels les tests ou les outils de débogage peuvent encore avoir besoin d’accéder, tout en réservant # aux quelques champs qui doivent être protégés contre toute forme d’inspection en temps de exécution. Le compilateur les traite comme deux systèmes de visibilité distincts mais compatibles.
Les champs # fonctionnent-ils dans des environnements JavaScript plus anciens comme IE11 ?
Pas directement. Lorsque la cible de compilation est ES5 ou ES2015, le compilateur TypeScript recourt à des polyfills basés sur WeakMap pour simuler le comportement des champs #, ce qui augmente la taille du code et entraîne des coûts en termes de performance au moment de l’exécution. Si vous devez encore prendre en charge des navigateurs obsolètes, il vaut mieux utiliser les modificateurs private et se contenter d’une vérification uniquement en temps de compilation, plutôt que d’assumer les surcoûts liés aux polyfills.
Que se passe-t-il avec les champs # lors de la sérialisation JSON ?
JSON.stringify et les outils de sérialisation similaires ne peuvent tout simplement pas détecter les champs # ; par conséquent, tout objet les contenant sera sérialisé sans ces propriétés. Si vous avez besoin que une partie de cet état privé soit représentée dans le résultat, vous devez l’exposer délibérément — soit par le biais d’une méthode getter, soit en définissant une implémentation personnalisée de toJSON qui contrôle précisément ce qui est inclus.
Les sous-classes peuvent-elles accéder aux champs # de la classe parente ?
Non. Les champs privés d’ECMAScript appartiennent exclusivement à la classe dans laquelle ils sont déclarés, et cette frontière est absolue — une sous-classe ne peut ni lire ni modifier les champs # d’une classe parente, même indirectement via des méthodes protégées ou publiques. Cela constitue une différence significative par rapport aux modificateurs private, où le compilateur TypeScript autorise parfois l’accès depuis une sous-classe grâce à des assertions de type explicites.
Faut-il migrer les bases de code existantes des champs private vers les champs # ?
Seulement en cas de besoin concret — une véritable vulnérabilité de sécurité ou un exigence réelle en matière de confidentialité assurée à l’exécution. Comme cette migration constitue un changement majeur affectant les outils, le comportement de sérialisation et la conception des tests, il ne s’agit pas d’une action à entreprendre à la légère. Pour la plupart des applications internes, les modificateurs private offrent déjà un encapsulement suffisant, et le coût de la migration n’en vaut pas la peine. Priorisez d’abord ce passage pour les bibliothèques partagées, les API publiques ou les modules qui traitent des données sensibles.
Choisir le bon modèle de confidentialité pour votre codebase en 2026
Choisir entre les champs private de TypeScript et les champs # d’ECMAScript n’est pas une question de préférence arbitraire. Ce choix détermine si la garantie d’encapsulation reste valide après le compilateur, influence la manière dont du code externe peut interagir avec vos classes, et communique l’intention architecturale à toute personne qui maintiendra ultérieurement le code.
Préférez private lorsque vous contrôlez l’ensemble du graphe de dépendances et que vous donnez la priorité aux outils pour développeurs par rapport à des garanties strictes en temps de exécution. Optez pour # lorsque votre code s’exécute dans des environnements non fiables ou traite des données qui ne doivent en aucun cas être exposées via la réflexion. La plupart des bases de code du monde réel nécessitent finalement une combinaison des deux, appliquée de manière intentionnelle en fonction de ce que représente chacun des champs.
Prendre une mauvaise décision à ce sujet a tendance à se manifester en production : les invariants sont brisés par des mutations accidentelles, les identifiants de connexion s’échappent via l’infrastructure de journalisation, ou les ensembles de tests ne parviennent plus du tout à vérifier l’état interne. Tous ces problèmes peuvent être évités grâce à des conventions claires et à des règles architecturales documentées.
Les équipes qui développent des outils internes peuvent généralement compter sur les modificateurs private et profiter des outils matures qui leur sont associés. Les équipes qui publient des bibliothèques publiques ou travaillent dans des domaines sensibles en matière de sécurité ont besoin des garanties en temps de exécution fournies par les champs #. Ces deux approches sont également bien prises en charge dans l’écosystème actuel de TypeScript — le choix idéal dépend de votre modèle de menaces et du niveau de confiance que vous accordez aux utilisateurs de votre API.
Lectures complémentaires
- Ce que le support natif de TypeScript dans Node.js fait vraiment et ce qu’il ne fait pas — Cet article explique comment Node.js exécute les fichiers .ts de manière native grâce au suppression des types, pourquoi il omet la vérification des types, et quand vous avez encore besoin d’une véritable étape de compilation.
- Les patterns de conception React : de l’OOP classique aux hooks modernes — Il explique comment des patterns logiciels classiques tels que Singleton, Factory et Observer s’appliquent dans React, ainsi que des patterns spécifiques à React comme les HOC, les hooks et les composants composés.