Question

Why objects having instance of a class with private property in prototype throw when accessing private member?

Consider this code:

class Klass {
  #priv = 42

  get pub() {
    return this.#priv
  }
}

// these two correctly return 42
console.log(new Klass().pub);
console.log((new class extends Klass {}).pub)

// but this one throws "TypeError: Cannot read private member 
// #priv from an object whose class did not declare it"
console.log(Object.create(new Klass()).pub) // throws 

// even though
console.log(Object.create(new Klass()) instanceof Klass) // true
console.log(Object.getPrototypeOf(Object.create(new Klass())).pub) // 42

I thought that since I have a true instance of the Klass in the prototype chain then accessing Object.create(new Klass()).pub wouldn't throw.

Is this is by design? And if it is then why it was done this way? Also, what is a correct way to have a generic function to clone arbitrary class instances that behave similar to Klass?

Context:

I run into this issue when I was testing something with vitest. My code looked something like this:

import { it, expect } from 'vitest';

class Klass {
    #priv = 42;

    get pub() {
        return this.#priv;
    }
}

it('works', () => {
    expect(new Klass()).toMatchObject({
        make_this_test_fail: 'yup',
        pub: 42,
    });
});

My test obviously failed but instead of giving me nice diff showing that property make_this_test_fail wasn't found on the instance it showed

 FAIL  test/the.test.js [ test/the.test.js ]
TypeError: Cannot read private member #priv from an object whose class did not declare it
 ❯ Klass.get pub [as pub] test/the.test.js:7:15
      5| 
      6|  get pub() {
      7|   return this.#priv;
       |               ^
      8|  }
      9| }

I tracked this to this line, where actual comes from deepClone, which creates instances of arbitrary classes using Object.create here.

 3  71  3
1 Jan 1970

Solution

 2

Is this is by design? And if it is then why it was done this way?

Yes, this is by design: private fields are not inherited down the prototype chain, they only exist on the particular instance on which they were created during construction. This makes them completely undetectable from the outside, you cannot mess with private fields by e.g. exchanging the prototype of an object, and you cannot trick a method into creating a private field (by assignment, consider a setter in your example) on an object where it previously didn't exist.

Private fields are "hard private", very much like internal slots of builtin objects. They don't get forwarded by proxies either. The mental model of a private field is like a WeakMap storing the value per instance.

(That said, not everyone agrees it's a good design.)

Also, what is a correct way to have a generic function to clone arbitrary class instances that behave similar to Klass?

You cannot write such a generic function. Any class, whether using private fields or not, can have private state that is not clonable. The only way to make this work is give each class a clone method to clone itself in an appropriate way. Use a symbol for the method name if you like to avoid conflicts.

2024-07-13
Bergi