TypeScript private modifier vs ECMAScript private fields runtime visibility
0 reputation · 04 Jul 2023, 10:53 UTC
0 reputation · 04 Jul 2023, 10:53 UTC
TypeScript offers two primary mechanisms for restricting access to class members: the private modifier and ECMAScript private fields (using the # prefix). While both prevent access during development, they differ fundamentally in their implementation during transpilation to JavaScript.
The private keyword provides soft privacy, meaning it is enforced by the TypeScript compiler but erased in the resulting JavaScript output. In contrast, #private fields provide hard privacy enforced by the JavaScript engine at runtime, preventing access even when casting to any or using bracket notation.
This creates a discrepancy when designing libraries where strict data encapsulation is required to prevent external modification of internal state in production environments.
private and # impact the serialization of class instances?TypeScript’s `private` modifier is enforced only by the compiler. It is erased during transpilation and emitted as an ordinary JavaScript property, so external code can read or write it using bracket notation or a cast to `any`. ECMAScript `#` fields are enforced by the JavaScript engine at runtime through internal slots. External access throws a `TypeError`, and TypeScript also reports a compile-time error.
`JSON.stringify` includes TypeScript `private` members because they become normal own enumerable properties. It omits ECMAScript `#` fields entirely, so the serialized output lacks that internal state. If consumers need the full shape, add an explicit `toJSON()` method that returns the `#` fields you intend to expose.
From outside the class, `Object.keys()`, `Object.getOwnPropertyNames()`, and `for...in` do not expose `#` field names. Debuggers typically show `#` fields in local scope while execution is paused inside a class method, but external programmatic introspection cannot enumerate or read them without cooperation from the class.
This assumes TypeScript 3.8 or later and a target that supports native private fields. If `tsconfig.json` targets an older ECMAScript version, TypeScript may downlevel `#` fields using closures or `WeakMap`, which changes runtime identity and does not provide the same engine-level enforcement. Check your `target` setting before relying on hard privacy in production.
Missing diagnostic detail: What is your TypeScript `target` compiler option? It determines whether `#` fields are emitted natively or downlevelled, which changes the runtime guarantee.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 04 Jul 2023, 17:13 UTC
A practical distinction appears during serialization and inspection: JSON.stringify includes TypeScript private properties because they remain ordinary enumerable own properties, while #private fields are non‑enumerable and omitted entirely. Similarly, Object.getOwnPropertyNames() and Object.getOwnPropertyDescriptors() list the TypeScript private member but never surface the # field. In browser DevTools or Node’s inspector, the # field is hidden from the property tree and cannot be read or mutated via bracket notation (obj['#field'] throws), whereas the TypeScript private member is fully visible and writable at runtime. If your library must guarantee that internal state cannot leak into logs, snapshots, or consumer code that casts to any, #private is the only mechanism that survives transpilation. For environments lacking ES2022 support, a WeakMap‑based pattern can emulate hard privacy, though it adds runtime overhead.