TypeScript private vs Native JS Private Fields (#)

TypeScript's private keyword only enforces access restrictions during compilation, leaving properties visible at runtime and during serialization like JSON.stringify(). To achieve true runtime privacy, you must use JavaScript's native # private fields introduced in ES2022, which natively protect data at both compile-time and runtime.
Imagine you are building a payment integration and store an API key inside a client class. You add TypeScript's private modifier to the property, feeling confident that your key is safe from unintended access. But the second you serialize that class instance or inspect it in your browser's developer console, your secret key is displayed in plain text.
This happens because TypeScript's type system disappears the moment your code compiles down to JavaScript.
Why is TypeScript private not private at runtime?
TypeScript's private modifier is an abstraction that exists purely to provide feedback inside your IDE and during the build step. Once your code is transpiled into plain JavaScript, the private keyword is completely removed, converting the property into a standard public property on the object.
Think of TypeScript as a security guard standing outside a building checking badges. Once you pass the guard and step inside the building (the JavaScript runtime), the guard is gone. If another piece of code inspects your object at runtime using JSON.stringify() or Object.keys(), it bypasses all TypeScript checks.
Here is what happens when you serialize an object using TypeScript's private modifier:
class AuthToken {
private secretKey = "shh-secret-value";
}
const token = new AuthToken();
console.log(JSON.stringify(token));
// Output: {"secretKey":"shh-secret-value"}
Because the transpiler strips the private keyword away, the property remains fully enumerable and visible at runtime.
How do JavaScript # private fields guarantee runtime privacy?
ECMAScript 2022 introduced native private class fields using the # prefix, which are enforced directly by the JavaScript engine at runtime. When you prefix a property with #, the runtime hides it from external access, reflection, and enumeration.
Instead of relying on build-step type checking, the JavaScript runtime allocates private fields using a hard-coded internal metadata structure. If external code attempts to access a # property, the engine throws a runtime TypeError immediately.
Let's update our example to use native private fields:
class AuthToken {
#secretKey = "shh-secret-value";
}
const token = new AuthToken();
console.log(JSON.stringify(token));
// Output: {}
Because #secretKey is enforced natively at runtime, JSON.stringify() completely ignores the property, preserving your data boundaries.
What is the difference between private and # private fields?
Choosing between private and # comes down to whether you need design-time type safety or true runtime isolation.
| Feature | TypeScript private |
JS Private Fields (#) |
|---|---|---|
| Enforcement Phase | Compile-time only | Compile-time and Runtime |
JSON.stringify() |
Included in output | Excluded from output |
Object.keys() Visibility |
Enumerable / Visible | Completely hidden |
| Bypass via Type Casting | Possible ((obj as any)) |
Impossible |
| Minimum Target | Any ES version | ES2022 (or polyfilled) |
If you simply want to signal to teammates that a property shouldn't be touched directly, the private keyword is often enough. But if you are handling sensitive credentials, internal state machines, or library internals that must never be exposed or serialized, native # private fields are essential.
Frequently Asked Questions
Can you bypass JavaScript # private fields using any in TypeScript?
No, you cannot bypass # private fields using type casting like (instance as any).#field. Because runtime privacy is enforced directly by the V8 engine, attempting to access a private field outside its declaring class will throw an uncatchable syntax or type error.
Do # private fields show up in Object.keys() or for...in loops?
No, native # private fields do not appear in Object.keys(), Object.getOwnPropertyNames(), or for...in loops. They are omitted from object key lists entirely.
Does using native # private fields affect performance?
Modern JavaScript engines like V8 optimize # private fields aggressively. While historical polyfills added runtime overhead, modern native implementations run with virtually identical performance to standard class properties.
Comments
Join the discussionNo comments yet. Be the first to comment.



