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:
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.
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?
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
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.
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).
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.
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.
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.