Skip to content

Instantly share code, notes, and snippets.

@jeffmo
Last active January 11, 2024 06:05
Show Gist options
  • Star 80 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jeffmo/054df782c05639da2adb to your computer and use it in GitHub Desktop.
Save jeffmo/054df782c05639da2adb to your computer and use it in GitHub Desktop.
ES Class Property Declarations
@mischkl
Copy link

mischkl commented Aug 31, 2015

IMHO, if your goal is to share properties across all instances, adding them to the prototype as @PinkaminaDianePie @RobertWHurst @lukescott et al suggest is not the way to go. Instead you should just use the static keyword (attaching to the constructor), it's that simple. (Unfortunately, ES6 doesn't support declaratively defining static properties, only static methods, but this is something that should be rectified ASAP as there are perfectly good, safe use cases for them.) There are really almost no arguments I can think of for attaching non-method properties to the prototype, other than maybe compatibility with some libraries that expect this.

On the other hand, there are very valid use cases for being able to define instance properties as part of the class definition, just look at any Java or TypeScript code if you can't imagine them. At the very least it helps with documentation, since it's not really desirable to put JSDoc in the middle of your constructor. Also this proposal opens the way for private instance properties, which provide a much more user-friendly syntax for hiding data than having to use the comparatively unfamiliar symbol syntax.

@jamesplease
Copy link

I know nothing, but one syntax that I like to support static/proto/instance props is...

class Base {
  // attached to the constructor
  static staticProp = 42;
  // attached to the prototype
  proto anotherProp = 43;
  // attached to the instance
  yetAnotherProp = 44;
}

@tbranyen
Copy link

I'm really confused about the class sugar syntax and the proposal here. Why do values need to be directly assigned to this? As @sporto already mentioned this is possible and very clearly expressed by assigning to this inside the constructor. What is missing is the ability to define properties directly on the prototype.

I'm just thinking in terms of what class is trying to accomplish and how it's currently not backwards compatible with things like React.createClass or Backbone.*.extend. Sure new developers are protected, but existing code will be a pain to mix between ES6 classes. Personally I think attaching to this is more confusing and less consistent than attaching directly to the prototype.

@mischkl
Copy link

mischkl commented Sep 1, 2015

@tbranyen as was mentioned in the initial description, attaching directly to the prototype has the inherent problem that if you change the value it is reflected in all instances of that class. So it's not really very helpful to be able to do that. (In fact the uselessness/anti-patternness of this is exactly the reason it was excluded from the ES6 spec.) On the other hand, if your intention actually is to share across all instances, that's what static properties are for. What's missing is the ability to define instance fields declaratively, in a way that facilitates documentation and potentially helps with compiler optimizations about what an instance of a class "should" look like. This is a feature that is pretty much expected of the class pattern for people coming from other languages, so it's not like it's re-inventing the wheel.

Admittedly the terminology "class properties" is kind of confusing since it sounds like they are saved on the class as opposed to only being defined there. Maybe this should be re-termed something like "class-defined properties", "class members" or "fields" to make it clearer.

@jmeas I like it! Add private to that list and I'll sign up in a heartbeat. ;)

@tomascharad
Copy link

Hi, I'm a newbee on ES7, but would it be possible to declare an async property?

@dead-claudia
Copy link

@jeffmo

Would you mind moving this to its own repo? An issue tracker would be really nice for this.

@jeffmo
Copy link
Author

jeffmo commented Sep 14, 2015

And once again I'm duped by the lack of emails from gist comments (only this time I should've known better...) :(

I'm really sorry for the lack of interaction, everyone. Per @lukescott's and @IMPinball's suggestion I'm going to start moving this to it's own repo -- including various updates and bits of feedback. Hopefully then we can discuss by way of issues and have a good conversation here.

I'm planning to bring the proposal back to the TC39 meeting in a couple of weeks with everyone's feedback we've gotten thus far. After I've made some progress porting/"rebasing" the proposal to it's own repo, I'll post the link here.

@jeffmo
Copy link
Author

jeffmo commented Sep 14, 2015

Ok, and before I move on too far I'd like to address some of the feedback given above. This is all really great and I very much appreciate everyone taking the time to discuss!

Hopefully you'll all actually manage to see this comment somehow... :p

@lukescott:

Does the order the properties are defined in matter?

The order only matters if the initializers have side effects (or if there are duplicate initializers for the same property name). Because initializers are evaluated in-order, later initializations will overwrite previous ones if they're the same name or generally have overlapping side effects. This is most consistent with the rest of the language (especially class methods -- which may have duplicates where the last version wins).

As far as : vs = goes, if type hinting were added, would it need to be supported on objects as well?

Though slightly beyond the scope of this proposal, I suppose this is worth considering since it's related in terms of forward consideration: Syntactic type-hints in TypeScript and Flow have worked really well using the regular : syntax for most things, but for things like objects you're right -- there's not a clear answer straight out of the ES-Grammar box.

In TypeScript the solution is to define a structural type for the full object (in an interface or type alias or inline annotation) and then hint that the entire object meets this structural type at assignment/init-annotation positions. In Flow you can do the same or you can specify the type inline using a parenthesized expression-annotation on the value a la:

var myObj = {
  someTypedProperty: (someVariable: number)
};

Given these two options I think this issue has already been worked around successfully, so I suspect if ES adopts type annotation syntax one day it will likely pull from the existing work of tooling like this.

@PinkaminaDianePie:

I suspect the way I worded the gist text might've added some confusion here (and I'll take this as a hint to clarify things in my impending updates). The contents of this gist run through a few possible solutions before eventually arriving at the final one which represents the actual proposed feature.

Indeed I am proposing both "class" (AKA "static") properties as well as "instance" properties -- two roughly separate features (the former is notated by a prefix static keyword on the property declaration). The two are only related by the fact that they can both be specified in a class declaration/expression -- so I'll probably just split them into two proposals so they can be discussed without confusion. Additionally, I'll be sure to disambiguate what I mean by "class" property in my updates going forward.

we added property to class and it should be the same in all objects of this class.

(cc also @trusktr, @jabjab, @mkleehammer, @lukescott regarding this vs prototype)

It's true that methods sit on the prototype and this works well, but one of the reasons it works well is because it's relatively uncommon to actually mutate a method object in a way that instances depend on. Most of the time, one simply calls a method rather than storing data in it.

For properties on an instance, the typical use case will almost certainly be storage read and used by many instances. If we knew the storage was both frozen and immutable, then it wouldn't even really matter if it hits the prototype vs instances -- but since there's no guarantee of immutability (and in fact a lot of code requires and relies on such mutability), there is a much more likely chance that unintentionally-shared instance properties will pose a hazard.

Moreover, if you run through a lot of code today (not just ES6, but also ES5 and prior) you see that the most common/idiomatic patterns that developers use for instance properties isn't usually to assign them to the prototype, but to initialize them on this in the constructor.

For cases where one wishes to share a reference across all instances of the objects, there are two very good means of doing this: (1) Have the class close over a shared variable. All instances of the class will be able to read/update/mutate/etc this singleton variable or (2) specify the property as a "static" property (i.e. it sits on the class itself rather than on this) and simply reference it by way of MyClass.sharedThing. Note that "static" properties are also part of this proposal (though, as mentioned above, I will probably split them into a sibling proposal since static properties are far simpler to spec than instance properties).

Honestly, though, I think @PinkaminaDianePie articulated this idea very well when clarifying that the properties we're describing here aren't really "class" properties, but they are "instance" properties.

its only second way to add something to object in construct phase

That's right, the "instance property" portion of this proposal is only about adding a declarative means to specify instance properties (and a place -- expressively separate from initialization logic -- to add documentation about the properties). These declarations will not be required, so there's no reason expando properties in constructors would cease to work in the same way they do today.

The purpose of the proposal is to (a) limit boilerplate in some cases*, (b) provide a syntactic target for decorators that operate on non-method entities (i.e. "properties"), (c) improve expressiveness in some cases (i.e. documentation for properties for both humans and tools -- distinctly separate from initialization logic in the constructor), and (d) to open potential optimization opportunities for VMs who wish to make a more educated prediction of the expected layout of class-generated objects.

* Example where we can limit boilerplate with class properties and move property documentation out of the middle of :

class BaseClass { constructor() { /* some common init logic here */ } }

// Without properties
class ChildClass1 extends BaseClass {
  constructor() {
    super();
    this.id = generateUniqueId();
  }
  getId() { return this.id; }
}

// With properties
class ChildClass2 extends BaseClass {
  id = generateUniqueId();
  getId() { return this.id; }
}

But this code still ugly and my class poluted by static method. I think it will be better to do it this way:

[...]

it's much readable! i have default config in all objects of my class. i can simply override it in object constructor.

I totally agree! And, in fact, this will be possible with the "instance property" portion of this proposal :)

@jurca & @hax:

This looks nice, but I don't see how to define symbol properties. Have I missed anything?

Oops! This was oversight. Barring any unforeseen grammar issues, I don't see any reason not to include declarative computed properties rather than just IdentifierName. Thanks for pointing that out!

JS uses automatic semicollon insertion (ASI). Wouldn't this proposal break this language feature by making semicollons required for class properties?

Well, ASI only happens in some places...not all. In some (many?) places semicolons are already required just for purposes of disambiguation (the same reason we need them here). Consider, for example:

var b = 1, c = 2;

var a = b + c
([1, 2, 3]).length

This is a syntax error because it is parsed as var b = 1, c = 2; var a = b + c([1,2,3]).length -- but c is not a function, so it can't be "called"!.

@GothAck:

Could defining prototypal and instance properties perhaps look like:

class Demo {
  instance_prop = {a: 100};
  prototype proto_prop = 2;
  static meh = 'all the meh';
}

Assuming one can make the case for prototype properties, that certainly seems like a viable option to explore for the future (and it's also nice because it means the current proposal doesn't preclude this idea). However, I'd like to keep the scope of this proposal small right now since we need to get the fundamentals baked and tested. Since the pattern of setting properties on this in the constructor (rather than on prototype) is already most prevailing, I think this is a good place to start.

@jeffmo
Copy link
Author

jeffmo commented Sep 15, 2015

FYI: I've begun porting things over to this repo: https://github.com/jeffmo/es-class-properties/
Lets try to continue discussion on specific topics using issues there.

@vedmant
Copy link

vedmant commented Jan 2, 2016

I'm totally for class properties, I'm not JavaScript developer, but PHP developer, and first time I wanted to specify initial state in ES6 I tried to use: state = { count: 0 }; which is the first obvious thing to do, furthermore for static properties. This is easy and logical. I think it's good Idea to borrow some classes logic from different languages, like Traits for mixing from PHP, interfaces for inverting dependencies, so on.

@GGAlanSmithee
Copy link

GGAlanSmithee commented Aug 18, 2016

@jeffmo Even if this have been moved, you might want to update the gist to show that this is currently in stage 2, not stage 0. (Or atleast it says stage 2 here, but this lists it as a stage 1 draft...)

*EDIT: it does look like it is indeed in stage 2, according to this list

@WeeHorse
Copy link

Accepting

class Whatever{
  someSymbolThatIsAFunction(){
    // yeah they call me a method but I am of the Function type
  }
}

but not

class Whatever{
  someSymbolThatIsNotAFunction = 1;
}

is to violate JS syntax basics.

@bounceme
Copy link

bounceme commented Jun 2, 2017

so are semicolons required after each instance property set using this proposed syntax? otherwise a criticism i have is that the class body is messier and looks too much like any other set of statements of a regular function, and parsing things like *ident (){} is much harder

@littledan
Copy link

No, semicolons are not required; they follow normal ASI rules and may come automatically. The grammar contains semicolons, just as it does at the end of normal statements which are also subject to ASI.

@dawsbot
Copy link

dawsbot commented Jun 29, 2017

Should be updated from "stage 0" in the description of this gist to "stage 2" @jeffmo. Thanks for the information 👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment