HackerReposA FIELD GUIDE TO OPEN SOURCE
ProSponsor the shelf♧ 0
← Back to top all-time
tc39
proposal-class-fields
HTML2017–2023
tc39 / proposal-class-fields · Archived

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals

32Distinct people
39Mentions
1Explicit recommenders
1.7KGitHub stars
REPOSITORY AT A GLANCE

What to know before exploring

What it is

Orthogonally-informed combination of public and private fields proposals

GitHub

Where it is used

No documented product use was verified in the last source check.

What people are saying

Watching how the language grows can be very interesting. I highly recommend taking a look at TC39 proposals and the process. For example, SmooshGate:…

otras used a recommendation phrase. One indexed comment matches. This is a phrase match, not a verdict.

No classified warning is attached to this page. Read the discussions before treating silence as approval.

Also in these discussions

Mentioned in the same Hacker News threads. That is not proof they are substitutes.

This week on Hacker News

Nov 7–Nov 14 UTC · Direct repository links and separately labeled related project stories. Indexed through 11/14/2026, 12:54:37 PM UTC.

No direct repository links were found in the indexed HN discussions over the past seven days. Older discussions are available below.

Mentions over time

HN mentions by year39 dated mentions

Observed mentions: 2017-08-03 17:19:41 UTC to 2023-05-30 07:30:05 UTC.

051015202017-01-01 00:00:00 UTC to 2018-01-01 00:00:00 UTC (exclusive): 5 mentions; 1 submissions, 4 comments20172018-01-01 00:00:00 UTC to 2019-01-01 00:00:00 UTC (exclusive): 20 mentions; 1 submissions, 19 comments2019-01-01 00:00:00 UTC to 2020-01-01 00:00:00 UTC (exclusive): 10 mentions; 0 submissions, 10 comments2020-01-01 00:00:00 UTC to 2021-01-01 00:00:00 UTC (exclusive): 0 mentions; 0 submissions, 0 comments20202021-01-01 00:00:00 UTC to 2022-01-01 00:00:00 UTC (exclusive): 0 mentions; 0 submissions, 0 comments2022-01-01 00:00:00 UTC to 2023-01-01 00:00:00 UTC (exclusive): 0 mentions; 0 submissions, 0 comments2023-01-01 00:00:00 UTC to 2024-01-01 00:00:00 UTC (exclusive): 4 mentions; 2 submissions, 2 comments2023

Counts come from the repository’s dated source evidence. Index cutoff: 2026-11-02 08:16:59 UTC.

View mention counts and date ranges
UTC ranges include their start and exclude their end. Select a period to filter the source evidence.
PeriodStart (UTC)End (exclusive, UTC)MentionsSubmissionsComments
20172017-01-01 00:00:00 UTC2018-01-01 00:00:00 UTC514
20182018-01-01 00:00:00 UTC2019-01-01 00:00:00 UTC20119
20192019-01-01 00:00:00 UTC2020-01-01 00:00:00 UTC10010
20202020-01-01 00:00:00 UTC2021-01-01 00:00:00 UTC000
20212021-01-01 00:00:00 UTC2022-01-01 00:00:00 UTC000
20222022-01-01 00:00:00 UTC2023-01-01 00:00:00 UTC000
20232023-01-01 00:00:00 UTC2024-01-01 00:00:00 UTC422

From the project README

An excerpt from the repository, fetched Nov 13, 2026.

Read the project’s introduction
Class field declarations for JavaScript Daniel Ehrenberg, Jeff Morrison Stage 4 A guiding example: Custom elements with classes To define a counter widget which increments when clicked, you can define the following with ES2015: class Counter extends HTMLElement { clicked() { this.x++; window.requestAnimationFrame(this.render.bind(this)); } constructor() { super(); this.onclick = this.clicked.bind(this); this.x = 0; } connectedCallback() { this.render(); } render() { this.textContent = this.x.toString(); } } window.customElements.define('num-counter', Counter); Field declarations With the ESnext field declarations proposal, the above example can be written as class Counter extends HTMLElement { x = 0; clicked() { this.x++; window.requestAnimationFrame(this.render.bind(this)); } constructor() { super(); this.onclick = this.clicked.bind(this); } connectedCallback() { this.render(); } render() { this.textContent = this.x.toString(); } } window.customElements.define('num-counter', Counter); In the above example, you can see a field declared with the syntax x = 0. You can also declare a field without an initializer as x. By declaring fields up-front, class definitions become more self-documenting; instances go through fewer state transitions, as declared fields are always present. Private fields The above example has some implementation details exposed to the world that might be better kept internal. Using ESnext private fields and methods, the definition can be refined to: class Counter extends HTMLElement { #x = 0; clicked() { this.#x++; window.requestAnimationFrame(this.render.bind(this)); } constructor() { super(); this.onclick = this.clicked.bind(this); } connectedCallback() { this.render(); } render() { this.textContent = this.#x.toString(); } } window.customElements.define('num-counter', Counter); To make fields private, just give them a name starting with #. By defining things which are not visible outside of the class, ESnext provides stronger encapsulation, ensuring that your classes' users don't accidentally trip themselves up by depending on internals, which may change version to version. Note that ESnext provides private fields only as declared up-front in a field declaration; private fields cannot be created later, ad-hoc, through assigning to them, the way that normal properties can. Major design points Public fields created with Object.defineProperty A public field declarations define fields on instances with the internals of Object.defineProperty (which we refer to in TC39 jargon as [[Define]] semantics), rather than with this.field = value; (referred to as [[Set]] semantics). Here's an example of the impact: class A { set x(value) { console.log(value); } } class B extends A { x = 1; } With the adopted semantics, new B() will result in an object which has a property x with the value 1, and nothing will be written to the console. With the alternate [[Set]] semantics, 1 would be written to the console, and attempts to access the property would lead to a TypeError (because the getter is missing). The choice between [[Set]] and [[Define]] is a design decision contrasting different kinds of expectations of behavior: Expectations that the field will be created as a data property regardless of what the superclass contains, vs expectations that the setter would be called. Following a lengthy discussion, TC39 settled on [[Define]] semantics, finding that it's important to preserve the first expectation. The decision to base public field semantics on Object.defineProperty was based on extensive discussion within TC39 and consultation with the developer community. Unfortunately, the community was rather split, while TC39 came down rather strongly on the side of Object.defineProperty. As a mitigation, the decorators proposal provides the tools to write a decorator to make a public field declaration use [[Set]] semantics. Even if you disagree with the default, the other option is available. (This would be the case regardless of which default TC39 chose.) Public fields are shipping in Chrome 72 with [[Define]] semantics, and this decision on semantics is unlikely to be revisited. Fields without initializers are set to undefined Both public and private field declarations create a field in the instance, whether or not there's an initializer present. If there's no initializer, the field is set to undefined. This differs a bit from certain transpiler implementations, which would just entirely ignore a field declaration which has no initializer. For example, in the following example, new D would result in an object whose y property is undefined, not 1. class C { y = 1; } class D extends C { y; } The semantics of setting fields without initializers to undefined as opposed to erasing them is that field declarations give a reliable basis to ensure that properties are present on objects that are created. This helps programmers keep objects in the same general state, which can make it easy to reason about and, sometimes, more optimizable in implementations. Private syntax Private fields are based on syntax using a #, both when declaring a field and when accessing it. class X { #foo; method() { console.log(this.#foo) } } This syntax tries to be both terse and intuitive, although it's rather different from other programming languages. See the private syntax FAQ for discussion of alternatives considered and the constraints that led to this syntax. There are no private computed property names: #foo is a private identifier, and #[foo] is a syntax error. No backdoor to access private Private fields provide a strong encapsulation boundary: It's impossible to access the private field from outside of the class, unless there is some explicit code to expose it (for example, providing a getter). This differs from JavaScript properties, which support various kinds of reflectio
Read the full README on GitHub

What Hacker News says

39 sources
> - # private... not sure why they didn't just use the "private" keyword, but I don't care. I almost always use TypeScript anyways One of the reasons was to allow private and public fields of the same name, so that subclasses are free to add own public fields without accidentally discovering private fields of superclasses. There were many more considerations that went into the design: https://github.com/tc39/proposal-class-fields/blob/main/PRIV.... There was a heated debate about this and the choice of the # sigil back in 2015 at the time private fields were being designed: https://github.com/tc39/proposal-private-fields/issues/14.
In JavaScript and TypeScript features of the last 3 years
The # sigil has always bummed me out too. Supposedly it's because of this: > Why isn't access this.x? > Having a private field named x must not prevent there from being a public field named x, so accessing a private field can't just be a normal lookup. https://github.com/tc39/proposal-class-fields/blob/main/PRIV... Combined with this: > Why aren't declarations private x? > This sort of declaration is what other languages use (notably Java), and implies that access would be done with this.x. Assuming that isn't the case (see above), in JavaScript this would silently create or access a public field, rather than throwing an error. This is a major potential source of bugs or invisibly making public fields which were intended to be private. https://github.com/tc39/proposal-class-fields/blob/main/PRIV... But I still think it's weird.
In JavaScript and TypeScript features of the last 3 years
CPS is a very bad idea in JS at present. CPS in scheme works because proper tail call optimization exists, so your call stack doesn't explode. In current JS, the only way to make that work is performing a nextTick or setTimeout on every single function which trashes performance of simple things due to exiting your code back to the event loop all the time. <stands on soapbox> There's a special place in hell reserved for companies that deviate from standards because of petty disagreements. Proper tail calls have been a part of the JS spec for 5 versions now. They are implemented in Safari and in popular embedded engines like duktape. MS and Firefox refused to implement. They were in v8 behind a flag, but were removed. v8 devs pitched a fit about stack traces disappearing (even though they disappear across the event loop anyway). The webkit
Read the rest
team showed that Chicken scheme's shadow stack works just fine and it's been in production for a long time now (without any issues I'd add). Instead, they insisted that they wanted to revise the standard to require marking tail-recursive functions. They also talked about decreased performance, but JSC team seems to have implemented it with a minimal performance impact, so it's definitely possible. More importantly, JS is a language to make development easy -- we have wasm in development for when speed becomes an issue and it's faster anyway (the label proposal would have also solved the issue by giving devs a choice). They then dropped their own proposal, but still simply refused to re-add the feature as per the spec in a bid to force a spec change. I have some understanding of not turning a feature on until a newer proposal is considered, but when they dropped that proposal and still refused to implement, they lost all credibility in my estimation. https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-... https://www.more-magic.net/posts/internals-gc.html <soap-boxing intensifies> I think this is indicative of a general "we know better than you" attitude on the part of the engine designers (especially the ones that sit on the spec committee). Another amazing example of this is the private variable proposal. NEVER has a JS proposal received such outspoken disapproval. Complaints range from the mostly insignificant "it's ugly" to the important "it goes against JS's core prototypical nature" to the very important "refactoring and maintaining this will be a nightmare due to dedicated syntax and JS being so dynamic", "it looks like a normal JS property that is hidden, but it in fact has almost nothing in common with normal properties even though the access syntax looks similar" or "this screws with inheritance, subclassing, destructuring syntax, etc". Instead of listening, they locked down all dissenting discussions on the proposal's github and are going full steam ahead. EDIT: some discussions are unlocked (see below for a decent argument summary). https://github.com/tc39/proposal-class-fields/issues/150 Decorators, pipe operators, tuples, immutable records, and slice notation are all well-understood ideas. Most don't require huge amounts of work to implement, but result in large quality-of-life improvements for developers. Most importantly, they aren't really controversial at all and most are easily transpilable so devs could start using them today (unlike private vars which either don't actually transpile correctly or create a mountain of unreadable garbage) Why can't we work on these things that have such wide user impact instead of forcing through controversial changes that only benefit a very few people?
In A brief look at async-await
I feel like i'm spamming this link every time the private fields stuff comes up, but take a look at the FAQ, it explains the reasoning behind it and why they didn't go with alternate options (like underscore, private, or others). I was against it at first, but after reading through that document, i'm on their side now. It'll be a small but useful addition in some situations, and it's scope is extremely limited. https://github.com/tc39/proposal-class-fields/blob/master/PR...
In What's New in JavaScript for 2019
It's not just about keywords, it's about shadowing mostly [1]. For example, what does `this.foo` access in an object? the private property `foo`? the public property `foo`? What about if I'm using your object and put a new property `foo` on it after it's created, then what happens? None of the answers to those questions are really good. If the private shadows the public, then how do you access the public? If it's the other way around, then external users can break your private fields by adding a public of the same name, which kind of defeats the purpose... TypeScript can get away with it because it's not really enforced by the runtime, only the typechecker, so their `private` isn't really private outside of TypeScript. [1] https://github.com/tc39/proposal-class-fields/blob/master/PR...
In What's New in JavaScript for 2019
> Now onto bound methods: you are wrong in your code example; `instance.method()` does not require `method` to be a bound method; an unbound function is just fine. Ah, true. I looked at the code, and was blind to it :) > Understand this: React is not normal code. React was designed in a distinctly unusual way There's nothing unusual about React. > (they invented an rather substantial language extension to achieve it!) That "extension" is nothing but function calls [1] [2] Quote: "Each JSX element is just syntactic sugar for calling React.createElement(component, props, ...children). So, anything you can do with JSX can also be done with just plain JavaScript." > This is not JavaScript’s fault It is entirely Javascript's fault. That is, it's the fault of its haphazard development with reckless disregard to any long-term planning. Classes a
Read the rest
re deliberately designed to look and behave, for the most part, like classes in C++-like languages. So they hid the whole prototype chain away behind the class keyword... But it's still there, and you have to be always consciously aware of it. As for the rest of your example, and how "you would rarely need to bind methods", well even the class fields proposal binds methods in its examples. So much for "rare" [3] And with quite a few people using WebComponents which are also class-based, well... Just a literally the first example I found: [4] And of course class fields will behave like class fields in C++-like languages will behave. Unlike methods. Because reasons. And of course, the "solution" to that is to throw in another half-baked solution in the form of a decorator. Which will take another several years to go through the standardization stage. ¯\_(ツ)_/¯ [1] https://reactjs.org/docs/jsx-in-depth.html [2] https://reactjs.org/docs/react-without-jsx.html [3] https://github.com/tc39/proposal-class-fields [4] https://github.com/elmsln/lrnwebcomponents/blob/master/eleme...
In ES6 Classes
The `y = 10;` is a class field, which is part of a Stage 3 proposal, https://github.com/tc39/proposal-class-fields. It’s not yet finalised, though it’s getting there. No current browser supports it, and those that write their code thus depend on Babel or similar projects to transform the code into a property definition in the constructor. Chrome 72, the next release, introduces support for it. Decide for yourself whether all that counts as “perfectly legal JavaScript” or not! Now onto bound methods: you are wrong in your code example; `instance.method()` does not require `method` to be a bound method; an unbound function is just fine. The code is equivalent to `instance.method.call(instance)`, which (for an unbound function) calls it with `instance` as its context. The problem is when, at call time, you don’t access the function as a prop
Read the rest
erty on another object, like this: const fn = instance.method; fn(); Or `(1, instance.method)()`. Then you need to worry about what the context will be (in strict mode, it’ll be `undefined`, and so any property access on it will immediately blow up, which is good). Bound methods are created by Function.prototype.bind, or by arrow functions. In what I will choose to call “normal code”, it’s somewhere between very rare and not particularly common to need to bind a function manually. Understand this: React is not normal code. React was designed in a distinctly unusual way (they invented an rather substantial language extension to achieve it!), and although that language and design has turned out quite well in most ways, it also has some exceptionally nasty weaknesses and rather annoying ergonomic troubles; and the need to bind functions is one of them. This is not JavaScript’s fault, but is because React has gone for a VDOM approach that (I think?) requires object identity in such cases, for reasons of efficiency. In search of one form of ergonomics, they damaged another form that others don’t have to worry about very often at all. Such is life, and the nature of tradeoffs. Most of the trouble with needing bound functions happens with event handling. (Certainly not all, but most, in my experience.) In non-React, non-VDOM-based code, you might be doing something like this: const foo = document.createElement('foo'); foo.addEventListener('bar', event => { … }); … and that’d be perfectly fine there. And a whole lot more efficient at runtime than React, I might add. * If you needed `this`, then the arrow function got it for you automatically. Sometimes you would want a separate function to designate the event handler, and so you may want to call bind(), but it’s also just as likely that you’ll want to keep Event out of that method, and just do `foo.addEventListener('bar', event => this.handleBar(event.detail))`. Maybe, maybe not. From your earlier example: be careful with your `x = () => …` instead of `x() { … }`, because that instantiates a new function for every instance of the object, which is very inefficient for memory consumption and performance. Binding a method from the prototype is much more efficient (though still to be avoided where unnecessary). The most interesting approach to resolving this binding problem that React causes lies in the decorators proposal (currently at stage 2). https://github.com/tc39/proposal-decorators/blob/master/boun... is all about a @bound decorator. * Just be careful with adding event listeners, and remember to use bubbling.
In ES6 Classes
First, about accessing private fields on members of the same class: the spec will allow this via `other.#foo`. The reason I said that the difference between `object.foo` and `object.#foo` is not purely syntax is because they are two completely different actions. The first is a lookup in the object's property map/prototype chain. The second is a private field lookup. `object.foo`: 1. Check to see if there's a property named "foo" in `object`'s property map. 1a. If yes, return the value. 1b. If no, continue. 2. Does this object have a prototype? 2a. If no, return undefined. 2b. If yes, repeat these steps starting from 1 using this object's prototype. `object.#foo`: 1. Test to see if you're in a class. 1a. If no, throw a Syntax Error. 1b. If yes, continue. 2. Test to see if `object` is an instance of this cl
Read the rest
ass. 2a. If no, throw a Syntax Error. 2b. If yes, continue. 3. Test to see if the class defines a private field *#foo*. 3a. If no, throw a Syntax Error ? (Not positive on this one, it might just return undefined). 3b. If yes, continue. 4. Look up this class' private fields for `object`. 5. Get the value for *#foo*. In case it wasn't clear: the private field `object.#foo` is NOT a property on `object` named `foo` and marked private. It is a field which exists in a namespace only accessible to the class definition. Without the # sigil, there'd be two issues. See [0] for a pseudo example of how private fields really behave. The first issue comes from a decision made in the spec by the committee: private fields must not interfere with object properties. This essentially means that an object can have the private field #foo AND the object property "foo" (as I described here [1]). Without the #, the fields would have to exist in the same namespace as the object's properties. If you error out when `object.foo` is accessed outside of the class definition, it would not fit the spec's definition of "private". [2] [3] The second issue is that without the #, the compiler would need to determine at the call site which lookup method to use, and they decided this would incur too much overhead on every single lookup in the already complicated property lookup logic. 0: https://news.ycombinator.com/item?id=18707421 1: https://news.ycombinator.com/item?id=18706363 2: https://github.com/tc39/proposal-class-fields/blob/master/PR... 3: https://github.com/tc39/proposal-class-fields/blob/master/PR...
In V8 release v7.2

Showing the 15 most recent of 39 sources. Enable JavaScript to search and browse every indexed mention.

MAKE YOURSELF AT HOME

A few shortcuts

/
Search the collection
?
Open this guide
Esc
Close dialogs and navigation

Your saved repositories and interests stay in this browser.