TypeScript Access Modifiers in 2026: Why `private` Fields Beat `#` and When the Opposite Is True A developer explains the critical differences between TypeScript's compile-time `private` modifiers and ECMAScript `#` fields, noting that `private` offers no runtime protection while `#` fields enforce hard privacy via WeakMap storage. The post advises using `private` for controlled TypeScript-only codebases and `#` for libraries or sensitive data, highlighting the risks of mixing both patterns. private Fields Beat and When the Opposite Is True This article was written with the assistance of AI, under human supervision and review. Most privacy bugs in TypeScript codebases stem from misunderstanding the two fundamentally incompatible encapsulation models: compile-time private modifiers and runtime ECMAScript fields. Teams pick one arbitrarily, ship to production, then discover edge cases where their choice breaks catastrophically. The TypeScript private keyword offers zero runtime protection. The compiler enforces visibility during development, but the emitted JavaScript exposes every field as a plain public property. Any consumer importing the transpiled code bypasses the entire privacy contract. ECMAScript fields solve this with hard privacy. The JavaScript runtime enforces encapsulation using WeakMap storage, making truly inaccessible fields that no external code can reach. This prevents accidental breakage and secures sensitive state in untrusted environments. The choice between these patterns determines whether your encapsulation survives production. This distinction is critical. private modifiers disappear after compilation, leaving plain JavaScript properties accessible at runtime. ECMAScript fields enforce hard privacy through WeakMap storage that survives transpilation. private for type safety in controlled TypeScript-only codebases where compile-time checks suffice. Use when shipping libraries, working with dynamic imports, or protecting sensitive data from runtime inspection. private to changes your public API surface and breaks reflection-based tooling. Codebases need explicit conventions to prevent mixing both inconsistently. private because it feels familiar from other languages, then encounter silent failures when JavaScript consumers bypass the contract. The failure mode here is subtle but expensive. private Modifier: Compile-Time Only TypeScript's private modifier exists solely in the type system. The compiler prevents access during development, but the resulting JavaScript contains ordinary properties with no protection mechanism. This makes private a documentation tool more than a security feature. class UserSession { private token: string; private expiresAt: number; constructor token: string, ttl: number { this.token = token; this.expiresAt = Date.now + ttl; } isValid : boolean { return Date.now < this.expiresAt; } } const session = new UserSession "abc123", 3600000 ; // TypeScript error: Property 'token' is private // console.log session.token ; The emitted JavaScript looks like this: class UserSession { constructor token, ttl { this.token = token; this.expiresAt = Date.now + ttl; } isValid { return Date.now < this.expiresAt; } } const session = new UserSession "abc123", 3600000 ; // Works perfectly at runtime console.log session.token ; // "abc123" The private keyword vanished. Any JavaScript consumer can read or mutate the field directly. This matters in three scenarios: publishing libraries to npm, loading third-party modules dynamically, or working with reflection-based frameworks like serializers or ORMs. The advantage of private modifiers is simplicity. Developers familiar with Java or C adopt the pattern instantly. IntelliSense hides private members in autocomplete. Refactoring tools understand the visibility contract. The TypeScript compiler catches accidental leaks during code review. The failure mode appears when assumptions about the runtime environment break. A team building an internal dashboard assumes all consumers use TypeScript. Six months later, a Python service imports the transpiled JavaScript bundle and mutates session tokens directly. The privacy contract collapsed because it never existed outside the compiler. This pattern works when you control the entire dependency graph and enforce TypeScript everywhere. The moment you cross language boundaries or publish to public registries, private becomes a suggestion. : Runtime-Enforced Hard Privacy ECMAScript private fields use the prefix to create truly inaccessible class properties. The JavaScript runtime stores these in an internal WeakMap, making them invisible to reflection and external code. TypeScript preserves the syntax when targeting ES2022 or later. class SecureWallet { balance: number; encryptionKey: string; constructor initialBalance: number, key: string { this. balance = initialBalance; this. encryptionKey = key; } getBalance : number { return this. balance; } deposit amount: number : void { if amount <= 0 throw new Error "Invalid amount" ; this. balance += amount; } } const wallet = new SecureWallet 1000, "secret-key" ; console.log wallet.getBalance ; // 1000 // Runtime TypeError: cannot access private field // console.log wallet. balance ; The emitted JavaScript retains the syntax when targeting modern environments: class SecureWallet { balance; encryptionKey; constructor initialBalance, key { this. balance = initialBalance; this. encryptionKey = key; } getBalance { return this. balance; } deposit amount { if amount <= 0 throw new Error "Invalid amount" ; this. balance += amount; } } The browser or Node.js runtime enforces encapsulation. Attempting wallet. balance throws a syntax error in strict mode. Even Object.keys wallet returns an empty array because private fields exist outside the property enumeration system. The cost of fields is compatibility. Older transpilation targets like ES5 or ES2015 require polyfills that bloat bundle size. TypeScript generates WeakMap-based shims when targeting legacy environments, adding overhead for every private field access. This matters for libraries shipping to browsers with tight performance budgets. The other tradeoff is developer experience. Autocomplete cannot suggest fields from outside the class. Debugging tools sometimes hide private fields in object inspectors. Serialization libraries like JSON.stringify skip private fields silently, which surprises teams expecting complete object graphs. Private fields excel in three scenarios: protecting cryptographic keys or tokens, preventing API consumers from breaking internal invariants, and shipping code to untrusted environments where reflection-based attacks matter. The runtime guarantees trump convenience in these cases. The decision between private modifiers and fields comes down to trust boundaries and tooling requirements. Neither pattern dominates universally. The implication here is that teams need explicit conventions rather than defaulting to familiarity. Use TypeScript private when: field polyfills add unacceptable bundle weight.Use ECMAScript fields when: The patterns conflict when you need both reflection and runtime privacy. A common failure case: an ORM expects to enumerate all fields for database mapping, but fields disappear from property lists. The workaround involves explicit getter methods or metadata decorators, adding ceremony that teams resist. Another edge case appears in testing. TypeScript private fields allow test files in the same project to access internals through type assertions. ECMAScript fields require extracting testable logic into separate methods or using dependency injection patterns. Teams accustomed to testing private implementation details find this friction jarring. The 2026 landscape shows growing adoption of fields in security-critical libraries and persistence in private modifiers for internal codebases. TypeScript 5.7 treats both as first-class citizens with full inference and error checking. The choice is architectural rather than technical. A production codebase often needs both patterns serving different purposes. The key is consistency within logical boundaries: use private for internal implementation details and for security-critical fields. class APIClient { // Public configuration public readonly baseURL: string; // Compile-time private implementation detail private requestCache: Map