ESC

Type to search the knowledge base.

Private Class Fields

Hard privacy with #fields and #methods — syntax rules, brand checks, vs WeakMap closures, and what stays enumerable.

intermediate3 min read
  • javascript
  • private-class

_ prefixes are a suggestion. # private fields are enforced by the language: access outside the declaring class is a SyntaxError (static) or TypeError (wrong receiver).

class Counter {
  #n = 0;
  static #instances = 0;

  constructor() {
    Counter.#instances++;
  }

  inc() {
    this.#n++;
    return this.#n;
  }

  get value() {
    return this.#n;
  }

  static count() {
    return Counter.#instances;
  }
}

const c = new Counter();
c.inc();
// c.#n; // SyntaxError — outside class body

Rules that bite

  1. Declare before use — #x must appear as a class field declaration in that class.
  2. No dynamic access — this['#n'] is a normal property name string, not private.
  3. Subclass can’t touch parent privates — true encapsulation across inheritance.
  4. Presence check: #f in obj (private brand check).
class Base {
  #secret = 1;
  same(other) {
    // only works if other is a Base instance (has the brand)
    return this.#secret === other.#secret;
  }
}

Accessing #secret on an object that isn’t an instance of the class that declared it throws.

Private methods and accessors

class Token {
  #raw;
  constructor(raw) {
    this.#raw = raw;
  }
  #normalize(s) {
    return s.trim().toLowerCase();
  }
  equals(other) {
    return this.#normalize(this.#raw) === this.#normalize(other.#raw);
  }
}

vs closures / WeakMap

Approach Privacy Per-instance cost Ergonomics
#field hard engine-optimized best in classes
Closure in constructor hard function-per-instance methods if not careful factories
WeakMap hard map entry pre-# pattern
_private soft none convention only
// legacy WeakMap privacy
const _n = new WeakMap();
class Counter2 {
  constructor() { _n.set(this, 0); }
  inc() { _n.set(this, _n.get(this) + 1); }
}

Prefer # in modern class-based code.

Reflection and enumeration

const c = new Counter();
Object.keys(c);           // [] regarding #n
Object.getOwnPropertyNames(c); // no #n
JSON.stringify(c);        // '{}' if only privates

Privates don’t show up in common enumeration — good for secrets, bad if you expected them in logs. Expose explicit toJSON when needed.

Static private

class C {
  static #id = 0;
  static nextId() {
    return ++C.#id;
  }
}

Useful for registries without leaking module-level mutables… though module scope is also private to the module.

Interview answer (out loud)

“Hash private fields are true encapsulation: only code in the class body can touch them, including private methods. Subclasses don’t get access. There’s no dynamic name for them. Before #, we used WeakMaps or closures. Underscore is convention only.”

Private static and instances

class C {
  static #x = 1;
  #y = 2;
  static read(inst) {
    return C.#x + inst.#y; // static method can access instance privates of same class
  }
}

Same-class access is allowed; foreign classes cannot reach #y even from a static helper outside.

Ergonomics with TypeScript

TS supports # private and also private keyword (compile-time only). Prefer # when you need runtime enforcement; private in TS evaporates in emitted JS.

Further reading

Related guides