Skip to main content

Inheriting private class fields

Private fields in JavaScript are not inherited: #field is bound to the body of the class that declares it, so a subclass cannot write this.#field. That expression is a SyntaxError at parse time, even though the object physically carries the field because the parent constructor created it.

Theory

TL;DR

  • #field belongs to one specific class body, not to the prototype, so a subclass cannot see it.
  • Accessing this.#parentField inside a descendant is a SyntaxError, not undefined.
  • The same #secret name in a parent and in a child is two different, independent fields.
  • Public parent methods that read their own private field are inherited and work fine on a child instance.
  • To share data with descendants you use a getter or method, the _field convention, or a WeakMap.

Quick example

javascript
class Parent { #secret = 'parent secret'; showSecret() { console.log(this.#secret); } } class Child extends Parent { reveal() { // console.log(this.#secret); // SyntaxError: Private field '#secret' must be declared in an enclosing class } } const c = new Child(); c.showSecret(); // 'parent secret', the parent method sees its own private field

A Child instance really does hold #secret inside, because the Parent constructor set it. But the code of Child has no right to spell that name, so the engine refuses to compile the module at all.

How it works under the hood

When you declare #field, JavaScript creates a unique hidden identifier that is available only inside that class body. A private name is not a string and not a Symbol in the usual sense: it resolves lexically, like a variable, not through an object property lookup.

The consequence: identical names in different classes are different fields with no relation to each other.

javascript
class Parent { #secret = 'parent'; getParentSecret() { return this.#secret; } } class Child extends Parent { #secret = 'child'; getChildSecret() { return this.#secret; } } const c = new Child(); console.log(c.getParentSecret()); // 'parent' console.log(c.getChildSecret()); // 'child'

One object happily holds two fields named #secret: one keyed by the Parent class, one by the Child class. There is no shadowing and no conflict.

Why the language forbids this inheritance

It is a deliberate decision by the specification authors:

  • Strict encapsulation. Everything that starts with # is fully hidden and cannot be opened up by subclassing.
  • No name collisions. A library author can add #cache to a base class without breaking a subclass that also has #cache.
  • Easier engine optimisation. V8, SpiderMonkey and JavaScriptCore know the full set of private names of a class at compile time and can lay them out in fixed slots.

So extends inherits methods and public fields, but never gives access to the parent private fields.

How to share data with descendants

JavaScript has no real protected modifier, so one of three approaches is used.

Option 1: the _field convention. This is not privacy, it is an agreement not to touch the property from outside, but the field is ordinary and fully available to subclasses.

javascript
class Parent { _secret = 'available to subclasses'; showSecret() { console.log(this._secret); } } class Child extends Parent { reveal() { console.log(this._secret); // works } }

Option 2: a parent getter or method. Privacy is preserved, and the subclass works through the interface without knowing implementation details.

javascript
class Parent { #secret = 'parent secret'; getSecret() { return this.#secret; } } class Child extends Parent { reveal() { console.log(this.getSecret()); // 'parent secret' } } new Child().reveal();

Option 3: a WeakMap. The classic pre-ES2022 privacy technique. The data lives outside the object, in a module closure, so it is unreachable from outside, yet the parent can expose it to descendants through a protected accessor.

javascript
const privates = new WeakMap(); class Parent { constructor() { privates.set(this, { secret: 'shared secret' }); } get _data() { return privates.get(this); } } class Child extends Parent { reveal() { console.log(this._data.secret); // 'shared secret' } } new Child().reveal();

A WeakMap holds no strong reference to its key, so the entry disappears together with the instance and there is no memory leak.

Comparing the approaches

ApproachPrivacyAvailable to subclassesReal protection
#fieldfullnoyes, enforced by syntax
_field (convention)partialyesno, convention only
WeakMapalmost fullyesyes, data lives outside the object
getter or methodthrough the interfaceyesyes, controlled

Common mistakes

  • Expecting undefined instead of an error. Touching someone else's #field is a parse time SyntaxError, so the whole module fails, not just that one call.
  • Assuming the same name means the same field. #secret in the parent and #secret in the child are two independent slots; you cannot override the parent field that way.
  • Thinking methods are not inherited either. Public parent methods are inherited normally and freely read their own private field inside.
  • Looking for protected in JavaScript. There is no such modifier; protected in TypeScript is a compile time check, and in the emitted JS the field stays public.
  • Declaring a private field only in the child constructor. The private name must be declared in the class body, otherwise assigning this.#field is a syntax error as well.
  • Confusing #field with WeakMap privacy. #field is unreachable even in a subclass, while a WeakMap hands the data to anyone who holds a reference to the map itself.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.