🚀 Executive Summary

TL;DR: TypeScript’s `private` keyword offers only compile-time privacy, becoming public JavaScript, while the native `#` syntax provides true runtime privacy enforced by the JS engine. They coexist due to backwards compatibility, making `#` the recommended choice for robust, truly private class fields, especially in new projects or libraries.

🎯 Key Takeaways

  • TypeScript’s `private` keyword enforces privacy only at compile-time and is erased during compilation to JavaScript, making the property publicly accessible at runtime.
  • ECMAScript Private Class Fields (`#` syntax) provide true runtime privacy, enforced by the JavaScript engine itself, making properties inaccessible from outside the class.
  • For new code or library development, prioritize using the `#` syntax for private fields to ensure robust runtime privacy and prevent unexpected access or reliance on internal implementation details.

Why doesnt TS merge `private` and `#` syntax in the language?

TypeScript’s `private` keyword offers compile-time checks, while JavaScript’s `#` syntax provides true runtime privacy. We explore why they coexist due to backwards compatibility and which one you should use in your projects to avoid unexpected bugs.

The Two `private`s: A Senior Engineer’s Take on TypeScript’s Identity Crisis

I remember a frantic Tuesday night, the kind every DevOps lead dreads. Pagers were going off, our main dashboard was bleeding red, and our `order-processing-service` was throwing cryptic errors in production. After an hour of digging, we found the culprit. A junior dev, trying to be helpful, had written a small vanilla JS utility script for a hotfix that directly accessed and modified a property on one of our core TypeScript classes. “But that property was `private`!” he said, completely baffled. And he was right, it was. But it was `private`, not `#private`, and that small difference cost us a two-hour outage. That night, the distinction between TypeScript’s “suggestion” and JavaScript’s “law” became painfully clear to our whole team.

So, What’s the Real Difference?

This whole situation feels confusing because, on the surface, both `private` and `#` (the hash/pound symbol, officially called ECMAScript Private Class Fields) seem to do the same thing: hide properties from the outside world. The key difference isn’t in the *intent*, but in the *enforcement*.

  • `private` is a TypeScript Lie: Okay, “lie” is a strong word. It’s more of a “gentleman’s agreement”. When you declare a property `private`, the TypeScript compiler will scream at you if you try to access it from outside the class *in your TypeScript code*. But when that code gets compiled down to plain JavaScript, that `private` keyword vanishes. It’s gone. The property is just a public property on the resulting JavaScript object, accessible to anyone.
  • `#` is a JavaScript Truth: The hash syntax is a native JavaScript feature. When you use `#myProperty`, it creates a truly private field. The JavaScript engine itself enforces this privacy at runtime. There’s no way to access it from outside the class, period. No reflection, no sneaky `object[‘#myProperty’]` tricks. It’s a steel-walled safe.

Here’s the breakdown in a way that’s easy to see:

Feature TypeScript `private` ECMAScript `#` (Hash)
Enforcement Compile-time only. Enforced by the TS compiler. Runtime. Enforced by the JavaScript engine itself.
Result in JS Becomes a regular public property. The `private` keyword is erased. Remains a truly private field, inaccessible from the outside.
Reason for Existing Created by TypeScript years before JS had a private field solution. The official, standardized JavaScript language feature.

The reason they weren’t merged is simple and painful: backwards compatibility. If the TypeScript team suddenly made the `private` keyword behave like `#`, it would break countless existing codebases that might (even accidentally) be relying on the fact that `private` properties are accessible in the compiled JavaScript. It would silently change runtime behavior, and that’s a cardinal sin in language design.

Okay, I Get It. So How Do We Manage This?

Theory is great, but we need practical solutions that don’t cause production outages. Here are the three main strategies I push for on my teams.

Solution 1: The “Team Convention” Fix

For most internal projects, the simplest solution is to just pick one and enforce it. Create a rule, document it, and use a linter to make it stick. Consistency is more valuable than endless debate here. If you’re starting a new project, I’d lean towards using `#` for true privacy. If you’re on a legacy codebase that already uses `private` everywhere, just keep using it unless you have a compelling reason not to.

You can enforce this with ESLint. For example, to enforce `#` over `private`, you can configure your `.eslintrc` file.


{
  "rules": {
    "@typescript-eslint/prefer-private-members": "off", // Turn off the rule that prefers `private`
    "@typescript-eslint/no-extraneous-class": ["error", { "allowWithDecorator": true }]
  }
}

Warning: Be careful when applying new linting rules to a massive, old codebase. You might want to introduce it as a ‘warning’ first before making it an ‘error’ that breaks your CI/CD pipeline. Don’t be the person who creates 5,000 new errors on `main`.

Solution 2: The “Right Tool for the Job” Approach

This is the more nuanced, senior-level answer. You understand the difference, so you use them both intentionally.

  • Use `private` when you want to signal “this is an internal implementation detail” to other TypeScript developers on your team, but you don’t need hard runtime enforcement. It’s slightly more flexible if you have, for instance, a unit test that needs to peek at an internal state (though that’s often a sign of a bad test!).
  • Use `#` when you are absolutely, positively sure that a piece of state should never be touched from outside the class. This is for critical security-related data, or internal state that, if modified, would completely break the object’s integrity. Think of it as your “do not cross” line in the sand.

In our `order-processing-service`, the customer’s payment token should have been a `#` field. The internal cache key? `private` is probably fine.

Solution 3: The “Library Author” Defensive Stance

Are you writing a package, a framework, or any piece of code that will be consumed by other developers, potentially in a plain JavaScript environment? If so, this is a no-brainer: use `#` for everything you want to be private.

You have zero control over how people will use your library. Someone *will* try to access that `private` property because they can. They’ll build their application relying on your internal implementation detail. Then, when you release a new version and refactor that “private” property, their application will break, and they will blame you. Using `#` protects your library’s internals and protects your users from themselves. It’s a non-negotiable part of writing robust, public-facing code.

The Bottom Line

At the end of the day, this isn’t an oversight by the TypeScript team; it’s a consequence of building a language on top of another that is constantly evolving. The `private` keyword is a legacy feature from a time before JavaScript had a better answer. The `#` syntax is that better answer.

My advice? For all new code, default to `#`. It does what you think it does, and it will save you from a late-night debugging session. For existing code, be pragmatic. But whatever you do, make sure you and your team understand the difference. Don’t wait for a production outage to learn this lesson the hard way.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ What is the fundamental difference between TypeScript’s `private` and JavaScript’s `#` private fields?

TypeScript’s `private` is a compile-time construct that signals intent to the TypeScript compiler but vanishes in the compiled JavaScript, making the property public. JavaScript’s `#` (ECMAScript Private Class Fields) is a native runtime feature that enforces true privacy directly within the JavaScript engine, making the field inaccessible from outside the class.

âť“ How does TypeScript’s `private` compare to ECMAScript’s `#` in terms of enforcement and use cases?

TypeScript’s `private` offers compile-time checks, suitable for signaling internal details within a TypeScript codebase where runtime enforcement isn’t strictly critical or some flexibility (e.g., for testing) is desired. ECMAScript’s `#` provides immutable runtime privacy, ideal for critical data, security-sensitive information, or when authoring libraries where external access must be absolutely prevented.

âť“ What is a common pitfall when relying on TypeScript’s `private` keyword, and how can it be avoided?

A common pitfall is assuming TypeScript’s `private` provides runtime privacy, leading to unexpected access and modification of ‘private’ properties in compiled JavaScript, potentially causing production issues. This can be avoided by using ECMAScript’s `#` syntax for properties that require true runtime privacy, especially in shared libraries or critical application components.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading