· new features

Private Fields in TypeScript: What’s the Difference Between `private` and `#`?

Private fields in TypeScript can be declared with either "private" or "#". The private keyword only enforces privacy at design time through the TypeScript compiler, while the "#" syntax creates Private Elements that are also protected from outside access at runtime. Private Elements provide stronger encapsulation because their fields remain completely hidden from anything outside the class.

If you have worked with classes in TypeScript for a while, you have probably used private fields. They are a neat way to say: this part of the class is not meant for the outside world. In other words, they support encapsulation, the object-oriented principle that an object should hide its inner workings and only expose a clean surface to the outside.

Then you bump into this newer syntax with a # in front of it. In technical terms these are called Private Elements. Suddenly you are wondering, are these the same thing, and which one should you actually use?

Contents

Private the TypeScript way

TypeScript introduced the private keyword early on, long before JavaScript itself had any concept of real privacy. It is enforced entirely by the TypeScript compiler. When you mark something private, TypeScript will stop you from touching it outside the class during development time:

class Counter {
  private value = 0;
 
  increment() {
    this.value += 1;
  }
 
  read() {
    return this.value;
  }
}
 
const c = new Counter();
c.increment();

At runtime, private is only a convention. It still shows up as a property on the object, and reflection tools like Object.keys will happily list it. Encapsulation here is real in the TypeScript type system, but not airtight once the code is running in JavaScript:

// Still reachable at runtime with a little mischief
console.log((c as any).value); // 1
console.log(c['value']); // 1

Private the JavaScript way

The ECMAScript standard introduced true private fields using the # prefix. This was standardized in ES2022, much later than TypeScript’s private access modifier. Unlike TypeScript’s compile-time check, # fields are enforced by the JavaScript engine itself:

class SecureCounter {
  #value = 0;
 
  increment() {
    this.#value += 1;
  }
 
  read() {
    return this.#value;
  }
}
 
const sc = new SecureCounter();
sc.increment();

These fields embody encapsulation at the runtime level. The field is stored in a hidden slot. It will not show up in reflection, it will not serialize to JSON, and there is no way to reach it from the outside:

// Fields cannot be accessed at compile time and runtime
// sc.#value; // Syntax error
console.log(sc['#value']); // undefined
console.log(Object.keys(sc)); // []

The testing angle

With private modifiers, tests can peek inside the fields if they want to. You can cast to any or use bracket notation (instance['field']). Sometimes that is handy, sometimes it couples your tests to implementation details:

class Service {
  private client = { name: 'dev' };
 
  ping() {
    return 'ok';
  }
}
 
const s = new Service();
console.log(s['client'].name); // works!

With #, the option simply disappears. Tests must go through the public API:

class ServiceStrict {
  #client = { name: 'prod' };
 
  getClientName() {
    return this.#client.name;
  }
}
 
const s2 = new ServiceStrict();
console.log(s2.getClientName()); // works!

It is stricter, but it pushes you toward designing classes with clear, testable interfaces instead of relying on peeking under the hood.

Back to Blog