Now, let’s settle a classic TypeScript confusion: the difference between the private keyword and the new JavaScript #private fields. You’ve probably seen both, and if you’re like me, your first thought was, “Great, two ways to do the same thing. Why?” Well, my friend, they are not the same thing, and understanding the distinction is the difference between writing solid, future-proof code and accidentally creating a public API out of your internal state.

TypeScript’s private is a compile-time illusion. It’s a gentleman’s agreement between you and the compiler. The compiler will throw a fit in your editor if you try to access a private member from outside its class, but once it transpiles your beautiful TypeScript down to JavaScript, that protection vanishes into thin air. Poof. It’s just a regular property in the output.

class Illusionist {
    private secret = "abracadabra";

    revealTrick() {
        console.log(this.secret); // Works fine
    }
}

const magician = new Illusionist();
console.log(magician.secret); // 💥 TS Error: Property 'secret' is private.
// But if you check the compiled JS... it's just a plain object property.

Check the JavaScript output. It’s utterly normal:

class Illusionist {
    constructor() {
        this.secret = "abracadabra"; // Look! No privacy!
    }
    // ... methods
}

Any other JavaScript code, or even your own if you use a // @ts-ignore, can freely access magician.secret. It’s a design-time tool, not a security feature.

Enter the #Private Field (The Real Deal)

JavaScript finally got native classes and then, bless its heart, it got real private class fields using the # syntax. This isn’t a compiler trick; it’s a hard, runtime-enforced language feature. The # prefix makes the field actually private. The JavaScript engine itself will prevent you from accessing it outside the class.

class RealMagician {
    #realSecret = "presto chango"; // This is the # syntax

    revealTrick() {
        console.log(this.#realSecret); // Also works fine
    }
}

const realOne = new RealMagician();
console.log(realOne.#realSecret); // 💥 Syntax Error EVERYWHERE:
// - TS: Property '#realSecret' is not accessible outside class 'RealMagician'
// - JS: Uncaught SyntaxError: Private field '#realSecret' must be declared in an enclosing class

The compiled JavaScript is a sight to behold:

class RealMagician {
    #realSecret; // The engine knows this is special
    constructor() {
        this.#realSecret = "presto chango";
    }
    // ... methods
}

You cannot access it at runtime, period. It’s not a property on the object. Try Object.keys(realOne)—you won’t see #realSecret. This is genuine encapsulation.

So Which One Should You Use? (The Practical Guide)

This is where it gets fun. The answer is, as always, “it depends,” but my default stance has shifted heavily towards #private for new code.

Use #private when:

  • You want real, runtime privacy. This is crucial for libraries or APIs where you truly don’t want consumers poking at internal state, even in a pure JS context.
  • You’re building for the modern web. All major browsers support it.
  • You want to avoid naming collisions. Since #fields are truly private, a subclass can also have a #field with the same name without clashing with the parent’s. Try that with TypeScript’s private and you’ll just get a warning (if you’re lucky).

Stick with TypeScript’s private when:

  • You need to support older runtimes that don’t have # support (though if you’re transpiling with something like Babel, this is a moot point).
  • You’re adding types to an existing codebase and want a gradual migration. Renaming everything to # is a breaking change.
  • You’re writing code that will be serialized or inspected frequently. Since #fields aren’t actual properties, they’re a pain with JSON.stringify or debugging. You’ll need to write custom toJSON methods or getters, which is just more work.

The Quirks and “Oh, Come On!” Moments

Here’s the real trench knowledge. The coexistence of these two systems creates some… interesting scenarios.

Pitfall #1: The Accidental Mixing. You can use both on the same class. Please, for the love of all that is holy, don’t. It’s a maintenance nightmare.

class Frankenstein {
    private tsPrivate = "I'm a TS secret";
    #jsPrivate = "I'm a JS secret";

    showTheMonster() {
        console.log(this.tsPrivate, this.#jsPrivate); // It works, but why would you?
    }
}

Pitfall #2: The Weird Output. Look at the compiled JS for a class with both. It’s a mess. The TypeScript private property is emitted as a normal property, while the #private field is handled by the engine. It feels like two different languages got stapled together—because they did.

Best Practice: Pick one. Be consistent throughout your project. My strong recommendation is to embrace #private for new greenfield projects. It’s the standard, it’s more robust, and it makes your intent clearer. Reserve the TypeScript keyword for the edge cases mentioned above or for when you specifically want that “soft” privacy for design-time checks only.

Remember, TypeScript’s goal is to model JavaScript, not replace it. The #private field is JavaScript. TypeScript’s private keyword is a convenient, but ultimately weaker, design-time pattern. Use the right tool for the job.