Private Class Fields
Hard privacy with #fields and #methods — syntax rules, brand checks, vs WeakMap closures, and what stays enumerable.
- 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
- Declare before use —
#xmust appear as a class field declaration in that class. - No dynamic access —
this['#n']is a normal property name string, not private. - Subclass can’t touch parent privates — true encapsulation across inheritance.
- 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
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.