[HN Gopher] JavaScript private class fields considered harmful
       ___________________________________________________________________
        
       JavaScript private class fields considered harmful
        
       Author : feross
       Score  : 93 points
       Date   : 2023-04-28 21:47 UTC (1 days ago)
        
 (HTM) web link (lea.verou.me)
 (TXT) w3m dump (lea.verou.me)
        
       | eyelidlessness wrote:
       | The article (and author's referenced tweets) center on private
       | field access failures with Proxy, but (and) the same problems
       | arise if you override subclass members which defer to their super
       | for private access. This is something that works _perfectly fine_
       | with the `private` keyword semantics in TypeScript which predate
       | runtime private class members. I agree that the actual runtime
       | behavior is harmful, and I will only use it very carefully with
       | very skeptical consideration of whether there's any value in
       | using it.
       | 
       | Fortunately symbols provide exactly the same runtime
       | encapsulation semantics with none of the footguns. It's a little
       | bit more ceremony, but you can achieve everything a private class
       | field would accomplish by using singleton symbols as long as you
       | don't export them or otherwise expose them to a consuming scope.
        
       | mst wrote:
       | Database appears to have gone _poof_ but somebody archived it
       | beforehand: https://archive.ph/ffvTF
        
       | migf wrote:
       | Yyyyyeah, classes in js are pretty bad full stop. For instance,
       | passing a method as an argument will do awful things unless you
       | bind it to a this first. Gross.
       | 
       | Still, it's interesting to me that Proxy is more valuable to
       | author than private member variables. Curious about some common
       | use cases.
        
         | eyelidlessness wrote:
         | Proxy is commonly used to wrap reactive object values, which is
         | the use case the author identified as an issue.
        
         | graypegg wrote:
         | Totally agree. This is very much a symptom of JS's class-ish
         | syntactic sugar. Access modifiers have to be a runtime check,
         | and be bolted onto an environment that was built around
         | function scope being the only way control access.
         | 
         | Using typescript to handle things like access modifiers at
         | compile time makes everything much more predictable.
        
           | dexwiz wrote:
           | Doesn't typescript just transpile to closure based access
           | control?
        
             | nosianu wrote:
             | TypeScript's "private" is only for type checks. It is
             | simply _removed_.
             | 
             | You may also want to read what I wrote a few days ago
             | already: https://news.ycombinator.com/item?id=35653653
             | 
             | TypeScript now supports both the ECMAScript #private - it
             | has no choice in the matter, since it's in the language now
             | - and their own development-time "private". It sounds messy
             | but that's the fault of those who wanted it _in the
             | language_ , as runtime checks.
             | 
             | I agree with the other comment, closures are far more
             | powerful and flexible. I understand that this OOP stuff in
             | JS was probably a nod to the many programmers in many
             | companies already used to it, to make JS more palatable for
             | them. It's easy to demand millions of people worldwide to
             | adapt, but in business reality it does not work that way.
             | So I grudgingly accept those new(er) things in JS as a
             | business decision and necessity, for the huge crowd of
             | people doing programming mostly just for the money, who had
             | to write more and more code for the web that used to be run
             | as installable software on Windows.
        
             | afiori wrote:
             | Closures are a much better "class" primitive than JS
             | classes
        
         | dexwiz wrote:
         | Even without classes there are a ton of footguns involving
         | 'this' and method scope. Fat arrow notation papered over a
         | bunch of issues. But I have lost more than a few hours confused
         | as to why changing between map and a loop completely broke/fix
         | behavior. Or seen codebases absolutely peppered with unneeded
         | 'bind(this)' like it would ward off evil. I had to spend some
         | time studying before I really grokked it.
        
         | pwdisswordfishc wrote:
         | > For instance, passing a method as an argument will do awful
         | things unless you bind it to a this first.
         | 
         | That was true long before classes arrived.
        
           | mst wrote:
           | It continues to sadden me that the implicit this injection
           | happens only on direct invocation -                   let res
           | = obj.method(arg);
           | 
           | and                   let m = obj.method;         let res =
           | obj.method(arg);
           | 
           | being different things seems like an unfortunate trade-off
           | (though not knowing the full reasons it was chosen to be that
           | way makes me wary of assuming it was the wrong decision
           | overall).
           | 
           | I've been known, for things where the object is e.g. a bag of
           | components, to loop at construction time and replace all of
           | my methods with pre-bound copies of themselves, but having to
           | make a conscious decision as to whether I need to introduce
           | that extra code (and work for the runtime per-instantiation)
           | still makes me grumble to myself every time.
        
             | llamaLord wrote:
             | Can you explain wrist you mean by those two examples being
             | different things?
             | 
             | Do you mean that 'res' evaluates to a different result in
             | each?
             | 
             | Or that when you call 'object.method()' vs calling 'm()'
             | you would get a different 'this' context?
        
       | auggierose wrote:
       | Interesting read. I am drawing the opposite conclusion: I should
       | avoid both Proxy and Vue.js!
        
       | andrewstuart wrote:
       | Private is a terrible concept.
       | 
       | Only two days ago I chose to rewrite a chunk of code simply
       | because I couldn't call a private function that did precisely
       | what I needed.
       | 
       | Also "considered harmful" is an overused cliche I wish people
       | would stop saying it.
        
         | n2d4 wrote:
         | > Only two days ago I chose to rewrite a chunk of code simply
         | because I couldn't call a private function that did precisely
         | what I needed.
         | 
         | It may have done what you needed in the current version of your
         | library, but just look at it from the perspective of the
         | library author. If they ever want to refactor, or maybe remove,
         | that private function, it would break a lot of people's code.
         | And even if it were marked private or deprecated, it would
         | still break a lot of people's code. You could say "but if your
         | code gets broken it's your own fault" and yes you're right, but
         | it wouldn't stop those people from loudly complaining to the
         | author.
        
           | realusername wrote:
           | What's the alternative then? Maintaining a fork? It's the
           | same update problems just with more hassle.
        
             | Nullabillity wrote:
             | Yes, maintaining a fork means that you're explicitly taking
             | on the maintenance burden from that point on, and are on
             | your own in terms of updates.
             | 
             | That's a reasonable commitment to ask for if you're poking
             | around in implementation details.
        
       | dimal wrote:
       | This just reinforces my bias that OOP is what is really harmful.
       | Really, what good reason is there to have data and behavior bound
       | to the same object? I've never seen a good argument for that.
       | It's just taken as a given. It seems nice and convenient, but
       | that convenience seems to always lead to unexpected complexity
       | like this.
       | 
       | A while ago I was looking for a color manipulation library, and I
       | saw her Color.js, and loved all the features. Then I saw the
       | bullet point, "Readable, object-oriented API". Nope. I'm not
       | touching that. I chose culori instead. It's just a collection of
       | functions that transform standardized color data objects. It's
       | like lodash for colors. It's been great to work with.
        
         | ActorNightly wrote:
         | >Really, what good reason is there to have data and behavior
         | bound to the same object?
         | 
         | Building in code APIs for libraries.
        
         | lioeters wrote:
         | I agree. Private class fields seem totally unnecessary when you
         | can simply "hide" functions and variables in a local scope and
         | not export/expose it. (Or use symbols as another comment
         | suggested.) I also don't want a library for color manipulation
         | to be using Proxy and other magic tricks, including "this". I
         | prefer plain functions that take any values they need as
         | explicit arguments, and simply return results - without
         | maintaining state, having side effects, pretending to be a
         | plain object, etc.
        
       | SSchick wrote:
       | Personally I feel like private JS class fields are against the
       | spirit of a scripting language and are harmful to debugging and
       | inspectability (in the way they are implemented).
       | 
       | I've stuck with TypeScript private fields which does exactly what
       | I need.
        
       | zeroCalories wrote:
       | It seems to me that private fields are a feature best handled by
       | a compiler. Such a runtime check will only be useful in very
       | limited situations.
        
       | i386 wrote:
       | Isn't the point that private fields remain private implementation
       | details of the class in just about every language that has them?
       | 
       | As a library author, if I declare something as private, it means
       | you the consumer, may damage internal state in some way by
       | directly mutating those fields.
       | 
       | If they do need to be read or mutated, that's a good case for
       | adding to the public API, not complaining that reflection tricks
       | can't get at something you shouldn't have access to. Not to
       | mention how incredibly fragile your consumer is going to be when
       | upgrading... I feel like the JS folks are learning the same
       | lessons the hard way that the Java folks did 25 years ago.
        
         | jmathai wrote:
         | Isn't most Javascript rendered useless by the time it is ready
         | to be upgraded because all of its dependencies have moved on to
         | not so greener pastures?
         | 
         | On a serious note. Keeping up with changes in the Javascript
         | community seems like a full time job. I don't know how they get
         | any work done.
        
           | zdragnar wrote:
           | This joke was already getting old 6 years ago.
           | 
           | The major frameworks of today were already well established
           | then, and the last really big update to the language in 2015
           | had already been in fairly wide spread use via transpilers
           | (traceur and later 6to5, which became babel).
           | 
           | Honestly, in the last 13 years, many mainstream languages
           | have gone through similar churn. JavaScript has gone through
           | more than most, to be sure, but nobody complains about Rust
           | or C# nearly so much.
        
             | beezlewax wrote:
             | JS is dependencies all the way down. Upgrading things can
             | be a nightmare
        
               | bilalq wrote:
               | And yet, I find myself struggling way more with
               | Objective-C/Swift and Java/Kotlin deps than I do with JS
               | ones. Dependency upgrades can be problematic, sure, but
               | JS is no worse than any other language. I never had to
               | change compiler flags or mutate internals of a library in
               | npm the way I need to do with cocoapods.
        
         | krono wrote:
         | There is actually a stage 2 proposal "Function Implementation
         | Hiding" that is being pushed through despite the many very
         | legitimate and thoroughly described concerns that people have
         | been voicing on GitHub and everywhere else this topic is
         | brought up.
         | 
         | https://github.com/tc39/proposal-function-implementation-hid...
        
           | pwdisswordfishc wrote:
           | What are the concerns, actually? I have seen this proposal
           | before, and I don't recall any significant concerns.
        
             | krono wrote:
             | A quick glance over the GitHub issue titles should give you
             | a general idea.
             | 
             | Whilst I don't personally share all of the concerns posted
             | there and even believe some of them are a little bit silly,
             | I have to say that I do not at all like the way most of the
             | discussions are abruptly cut short or simply left
             | unanswered, and how difficult questions or ideas that
             | aren't entirely in line with the original proposal are
             | immediately declared nonsensical/invalid or shot down
             | without any further exploration at all.
             | 
             | The above is a common theme in that repo and not difficult
             | to find. I've never seen that kind of behaviour in any
             | other proposal at that stage. Very sad to see.
             | 
             | https://github.com/tc39/proposal-function-implementation-
             | hid...
        
         | zdragnar wrote:
         | Eh, most of the time, if you use private, what you really
         | wanted was protected.
         | 
         | Private causes extensibility issues (no access to private
         | fields in child classes). Not sure if the article addressed
         | this or not because it's currently hugged to death.
        
           | mst wrote:
           | https://archive.ph/ffvTF
        
           | SwiftyBug wrote:
           | It does.
        
           | pwdisswordfishc wrote:
           | No, most of the time, if I use private, what I really wanted
           | is private. And it does exactly what I want. So speak for
           | yourself.
           | 
           | Protected plus subclassing is just poor man's dependency
           | injection; entirely dispensable.
        
         | iudqnolq wrote:
         | The article seems quite practical.
         | 
         | She wants her users to be able to use Proxies over her classes.
         | Private members make a class ineligible for proxying. So she'd
         | rather use private-by-convention.
        
           | lenkite wrote:
           | "So she'd rather use private-by-convention."
           | 
           | This effectively and automatically devolves to "public-by-
           | contract". Seen it happen so many times that you can't
           | convince me otherwise.
        
       | pwdisswordfishc wrote:
       | Proxies exist pretty much for one purpose and one purpose only:
       | to implement membranes, like described in
       | <https://github.com/ajvincent/es-membrane/>, where you build on
       | demand a whole parallel universe of proxies that stand in for
       | objects existing somewhere else, and have wrapper functions
       | convert between those proxies and real objects. Any other use is
       | incidental and unsupported.
       | 
       | Admittedly, not many people know this, so those who are not
       | deeply familiar with TC39 proceedings commonly think of proxies
       | as just a way to hook into arbitrary behaviours of a single
       | object, like property accesses, then are treated to a bitter
       | surprise when proxies don't work like what they thought or
       | wanted.
       | 
       | Still, the opening example is simply a misuse of Proxy, so it's
       | rather unconvincing.
       | 
       | That said, with something like
       | <https://github.com/littledan/proposal-new-
       | initialize#another...>, something resembling the popular
       | conception of Proxy might actually be made to work...
        
         | NoahKAndrews wrote:
         | Can you elaborate on what's wrong with using then for other
         | purposes?
        
       | 8bitsrule wrote:
       | Great thing about JS is you _never_ need to use any OOP.
        
       | afiori wrote:
       | Personally this only reinforces how JS should have never gotten
       | classes.
        
       | rektide wrote:
       | In general, the people writing language features out of fear
       | cause the bulk of the problems.
       | 
       | Js had coped quite well without having private fields. We had
       | conventions of using this._field that served fine, that cautioned
       | users sometbing was internal-ish without binding them, without
       | preventing experimentation.
       | 
       | The private function implementation is more bad language design
       | seeming on an inevitable course to make the language worse & less
       | capable. It's more fear, more adding nothing, more creating
       | friction, like private fields.
       | 
       | That all said, it doesn't seem like proxies ought be broken by
       | private class fields. That's another heap of scorn placed upon
       | this already tragic & harmful misfeature.
        
         | claytongulick wrote:
         | I was amongst many who argued vocally against this proposal
         | back when it was in stage 1.
         | 
         | There was a huge community backlash against it, which tc39
         | blithely ignored.
         | 
         | It made me lose confidence in tc39 completely.
         | 
         | Whatever their motivations are, they are clearly not influenced
         | by community feedback, at least not for this feature.
        
       | transfire wrote:
       | Had similar problems with Java developing for Android.
       | 
       | Nothing like having to reimplement dozens upon dozens of methods
       | all because you need one little change in one untouchable private
       | method.
        
         | sbdkrend wrote:
         | > you need one little change in one untouchable private method
         | 
         | Tell me you don't know how anything works while not saying you
         | don't know how anything works.
        
       | peter-m80 wrote:
       | I just avoid classes in js
        
         | nailer wrote:
         | Indeed. They're very much implemented for people that wanted
         | nineties style OO and who don't get scopes/closures.
        
       | 8n4vidtmkvmk wrote:
       | whahaha. i took the exact opposite approach and swore off proxies
       | years ago for exactly this kind of edge case. there's scenarios
       | where they just don't work and they'll bite you and i don't want
       | to invite more gotchas into my code.
        
         | afiori wrote:
         | The author is not interested in writing proxy code, their
         | interest is to allow other to use their classes together with
         | libraries that happen to use proxies.
        
       | jschrf wrote:
       | I can't recall a single time in JS/TS, C# or even Java where I
       | went "phew, glad i made that private!".
       | 
       | In TS, i default to protected for internal implementation
       | details.
       | 
       | Why would one hinder future extenders by default?
        
         | SigmundA wrote:
         | If I am understanding the issue here I don't believe C# would
         | suffer from this issue since reflection works not only on
         | public method access where those methods use privates
         | internally but in fact could access privates directly.
         | 
         | In C# private is more of suggestion than an enforced concept
         | since you can do anything you want with reflection, which is
         | very useful and necessary sometimes. Keeps normal code clean
         | and encapsulated while allowing one to go under the covers as
         | needed.
         | 
         | Seems very strange that not only can you not get to private
         | members directly in JS but even worse you can't call a public
         | method that use a private though a proxy, bizarre and against
         | what I would think JS to be.
        
         | thfuran wrote:
         | Unless you're going 100% immutable everything (which the
         | language isn't great for), advocating for making all members
         | public in java seems roughly as sane as advocating for
         | declaring all variables and returns as Object.
        
         | oweiler wrote:
         | Because extensibility should be a design decision.
        
           | mst wrote:
           | That feels like an argument for 'final' more than 'private.'
           | 
           | Personally, at least for library code, I tend to assume
           | -somebody- will extend any given class of mine at -some-
           | point and would prefer they had a reasonable time doing so,
           | but I use 'prefer' advisedly since there are absolutely
           | situations where doing so would almost certainly be a footgun
           | and in that case I'd rather dissuade the user before sinking
           | time into the idea.
        
           | chii wrote:
           | extensibility not thought of during design is a flaw. which
           | can be patched over if the actual internals are reachable.
        
             | everforward wrote:
             | Sure, but extensibility often comes with complexity. If I'm
             | making something non-extensible it's to keep things simple
             | or to prevent a bunch of bugs around people trying to
             | extend something with complicated state.
             | 
             | They always have the nuclear option of forking and making
             | those fields no longer internal. They'll have to take on
             | the maintenance overhead that I shirked, but that should be
             | fine if they need it that badly.
        
       ___________________________________________________________________
       (page generated 2023-04-29 23:02 UTC)