private vs static private

After figuring out the best way of working around the known footguns of private fields, I shot myself in the foot yet again. Is this supposed to cause an error?

class Test {
    static #foo;
    #foo;
}

This throws "Uncaught SyntaxError: Identifier '#foo' has already been declared". This is surprising in the face of:

class Test {
    static foo;
    foo;
}

having no issues. While I'm not saying I don't understand why this might be (something to the effect of private field names belonging to the class itself regardless of whether or not they're static). However, I am saying that this is surprising and unexpected behavior.

The problem here is that although all the private names for a class are kept in the same bucket, and therefore cannot duplicate, this makes no sense in the face of examples like this:

class Test {
   static #foo;
   #bar;

   test() {
      try {
      console.log(`Test.#bar = ${Test.#bar}`);
      } catch(e) { console.log(e); }
      try {
      console.log(`this.#foo = ${this.#foo}`);
      } catch(e) { console.log(e); }
   }
}

Since the static private field name will only create a slot on the constructor and the instance private field name will only create a slot on instances, it makes absolutely no sense to group both name pools together under a single unique constraint.

Yes, this is intentional, since each private name is distinct.

If it were permitted, how would you know whether obj.#foo is the static or the instance field? Public fields just create properties, and properties don't have this problem because their names aren't a statically known thing like PrivateNames are.

1 Like

Proposal discussion: Static private member with same private name as a private instance member

Yes, this was on purpose. I didn't see any important use cases, and allowing name sharing would increase complexity a lot.

This is yet another one of those cases that makes me wish I was a TC39 member. The solution to this problem is simple. If there is to be a static and instance private field with the same identifier in the the same class, then just make the private name the same. There's no conflict or issue with doing so that I'm aware of, and it doesn't violate any of the demands that I'm aware of that were exposed in the many discussions had in the github repo. So why not just do that?

That would be a surprising footgun for when someone wanted them to be different.

Additionally, an object can have both static and instance fields via the return override trick; what would happen then?

I get the problem. The surprise to me (which at this point shouldn't be surprising given the other choices made in this proposal) is that once again, TC39 prioritized the edge cases over the primary case. It's more common by far and large to have a property of the same name on 2 different objects than it is to have 2 properties of the same name on the same object (especially since that's impossible).

As such, the return override trick would indeed do something somewhat surprising in that case, but far more understandable and inline with how properties work, by overwriting the previously declared private field with the new value since the names would match. If you think about it, this is no different than feeding an already initialized instance to another function of the same class that can change the values. That would be a reasonable outcome.

It would throw right? If an object already has a private field installed, trying to install the field again throws.

class X extends (class { constructor(v) { return v} }) {
   #x;
}
let o = {};
new X(o);
new X(o);  // Uncaught TypeError: Cannot initialize #x twice on the same object

@rdking , the issue linked above by @senocular does mention:

And , if it's a syntax error now (as it is), that means if there's a strong use case for it later, it can be added later.

Did you have particular use cases in mind? I imagine most code would know if it's reading from the static class vs an instance.

There's no specific use case, just a general one. I have a use case, but it's fairly specific to something I'm working on. I've created a small library that provides the ability to share private data between classes. It's based on this thread I started:

The trouble came when using it to work on another library. The first library (cfprotected) uses a private field as a container for the shared data. I need both the instance and the static objects to have a shared container. It would be easier if they both had a shared container of the same name. This would make it easier than having to remember 2 different container names. That's how I ran into this issue.

The following:

class C {
  static #f = 'a';
  #f = 'b';

  static getF(v) {
    return v.#f;
  }
}

Would be roughly the same as:

FieldFactory util
function fieldFactory() {
  const {get, set} = class extends (class { constructor(v) {return v} }) {
    #f;

    static get(o) { return o.#f; }

    static set(o, v) {
      if (! (#f in o)) new F(o);
      o.#f = v;
    }
  }

  return {get, set};
}
class C {
  static #f = fieldFactory();

  static { C.#f.set(this, 'a'); }

  constructor() {
    C.#f.set(this, 'b');
  }

  static getF(v) {
    return C.#f.get(v);
  }
}

Would this work in your use case?

It's similar, but the notation is far clumsier than I'd like. Just the fact that I'm using private fields at all means I'm already doing some non-ergonomic things just to prevent issues with the code when used with other libraries. As we know, using private fields is an instant footgun when used in combination with non-membrane uses of Proxy (which a lot of useful libraries do).

Here's an example of the code I'm working on:

const { share } = CFProtected;
const TagBase = await import("jsapplib/jsTagBase");

export default class Menu extends TagBase {
    static #tagName = "js-menu";
    static #prot = share(Menu, {});    //Duplication #1: static #prot

    static {
        this.#prot.registerTag(this.#tagName);
    }
    static get tagName() { return this.pvt.#tagName; }
    static get observedAttributes() {
        return TagBase.observedAttributes.concat([ "caption" ]); 
    }

    #htmlCaption = null;
    #prot = share(Menu, {              //Duplication #2: instance #prot
        render() {
            let content = window.document.createElement("div");
            content.style = `
                padding-top: 0.125em;    
                padding-bottom: 0.125em;
            `;
            content.appendChild(window.document.createElement("slot"));
            this.pvt.#renderContent(content);
        },
        onCaptionChanged(e) {
            let match = e.detail.newVal.match(/_(\w)/);
            if (match.length > 0) {
                let key = match[1];
                this.pvt.#htmlCaption = e.detail.newVal.replace(`_${key}`, `<u>${key}</u>`);
            }
            else {
                this.pvt.#htmlCaption = e.detail.newVal;
            }
            let label = this.shadowRoot.querySelector("js-label");
            if (label) {
                label.caption = this.pvt.#htmlCaption;
            }
        }
    });

    constructor() {
        super();
        this.pvt.#addEventListener("render", this.pvt.#prot.render);
        this.pvt.#addEventListener("captionChanged", this.pvt.#prot.onCaptionChanged);
    }

    connectedCallback() {
        super.connectedCallback();
    }

    get caption() { return this.getAttribute("caption"); }
    set caption(v) { this.setAttribute("caption", v); }
}

The general idea is that this.#prot always refers to the protected container regardless of whether this is the constructor or the instance. Makes things easy to remember. However, since there's the potential for this class or its instances to be Proxy wrapped, I bind this.pvt to this during construction so that even after being Proxy wrapped, the class itself doesn't worry about Proxy interference in accessing private fields. Not very ergonomic, but very safe. Can you see why jumping extra hoops just because 2 different contexts cannot have a member of the same name makes things messier?

Sure, I can easily give up on having the consistency of this.#prot being the private container, but I don't see why I should have to. I understand the arguments given, but as always regarding the choices behind private fields, I find those arguments wanting for better logic.

Oh wow, speaking about unexpected things, I would have expected that to throw a TypeError: Cannot write private member #x to an object whose class did not declare it already on the first new X(o) call. Not that returning o from the super() constructor would "change" its class from Object to X. I thought one of the goals of the private member proposal was to allow simple and effective optimisations by preventing them to be dynamically added to arbitrary objects.

Doing that would have been deemed surprising as a class constructor just applies its own modifications to whatever is returned by the super-constructor if it can. This is the result of class being designed to appear to be syntactic sugar over the pseudo classes people had been manually coding for years instead of as a truly unique means of introducing type construction. Not necessarily the best choice, but not a bad one either. However, it does mean that it's almost irrelevant what other constructors the object returned by super() has passed through except for where the previous constructors' initializations prevent the current constructor from initializing.

It was also deemed early on that there was no harm in allowing this since only the methods of the classes in which the object passed through their constructor would ever be able to access those fields. Information you can't reach, see, or even detect exists is the same as not being there, right? (Proxy sits in the corner and laughs.)

Turns out there's another similar issue with static private. I'm sure it's this way by design given the way things were decided. However, I can't comprehend why this limitation exists and I'm hoping there's at least something that resembles a good reason.

class A {
   static #spvt = 'HHGTTG';
   static run() {
      console.log(`this.#spvt = ${this.#spvt}`);
   }
   #pvt = 42;
   run() {
      console.log(`this.#pvt = ${this.#pvt}`);
   }
}

class B extends A { }

B.run()
//TypeError: Cannot read private member #spvt from an object whose class did not declare it

I already get that #spvt doesn't exist on B. What I don't get is what the harm is in allowing the private field lookup to follow the same prototype search rules as properties, and allowing this call to the inherited run function to access the private field it is aware of.

That one is actually surprising to me. I thought that since B inherits from A, the B object would have the private fields of the A object installed.

No. I remember that one. It's like the concept of an "own property". If you go looking on an object for a property definition that it inherited, you won't find it without searching the prototype objects. The problem is that for whatever reason, they decided against doing this for private fields. I don't get why.

Ha right, A is only the proto of B, so this.#foo is somewhat useless for static functions, and using the class identifier (A.#spvt) is the only valid way then. That makes it cumbersome for anonymous classes. This is indeed unfortunate.

1 Like

In the end, this all just reminds me of the biggest issue I had with the proposal: they valued so many other "concerns" before the basic operations and use cases for classes. I'm pretty sure that if @ljharb or @bakkot, or any of the others answer this question, it will end up being another one of those cases where their decision went counter to the more intuitive standard practices for some obscure reason or to satisfy some obscure want. I would just like to know what it is and whether or not it's even reasonable to correct it at this point.

There is this: GitHub - tc39/proposal-class-access-expressions: ECMAScript class access expressions

For accessing the containing class using class. So even anonymous classes could access their static fields safely with class.#f

Looks like Java has the same restriction in terms of a static and instance field having the same name:

class C {
  static private int x = 1;
  private int x = 2;
}

output:

Main.java:4: error: variable x is already defined in class C
  private int x = 2;