Stage: 0
Class element definitions (CEDs) are a proposal for extending JavaScript classes with a syntax that mirrors the behavior of Object.defineProperty
.
class C {
x {
configurable = true;
enumerable = true;
writable = true;
value() {
console.log('hello!');
}
}
y { get; set; } = 123;
}
This would enable:
- Changing the
enumerable
,writable
, andconfigurable
properties of class elements in a declarative manner. - Grouping related element definitions, such as getters and setters, in a single location.
- Automatic definitions for getters and setters, which simplifies common decoration use cases.
- Definition of non-method values on class prototypes.
Currently, there are a number of portions of the JavaScript object model which are not easily accessible via class syntax. For instance:
- It is not possible to declaratively define a class field which is non-enumerable.
- It is not possible to declaratively define a non-method value which is assigned to the prototype of the class rather than the constructor or the instance.
- It is not possible to declaratively make a method non-configurable.
All of these use cases can be accomplished imperatively using Object.defineProperty
after the class has been defined, but it has been a longstanding goal to add a way for class syntax to accomplish these use cases and covers these gaps in mapping from class model to object model.
Originally, it was believed that decorators would be able to solve these use cases, and earlier versions of that proposal did solve them. However, it was determined that these capabilities were too dynamic - they would fundamentally require class definitions to be far more dynamic and less optimizable. As such, decorators no longer can change the enumerability, writability, or configurability of a class element, and so we would need a new language feature to do this. This new feature also needs to be statically analyzable so that the changes to the shape of the class can be determined at parse time.
CEDs provide this syntax, and also provide a convenient way to group related definitions (such as getters and setters on the same property name) in a single location. In addition, CEDs provide a way to create automatic accessors, which are useful for a variety of decoration use cases.
The syntax for CEDs is an identifier followed by a block in a class body (i.e. Identifier {...}
). This block may contain field assignments or method definitions for the following properties:
writable
- MUST be a class fieldenumerable
- MUST be a class fieldconfigurable
- MUST be a class fieldvalue
- can be a class field or a class method. CANNOT be an empty field.get
- can be a class field or a class methodset
- can be a class field or a class method
As such, it is a strict subset of the syntax of a class body. For example, to define a non-writable class field, you would do:
class C {
x { writable = false };
}
The values of the CED block are 1-to-1 with the options provided to Object.defineProperty
, and generally have the same meaning and effect. Restrictions are also the same, for instance it would be a syntax error to have both value
and get
or set
in the CED, since that is an invalid combination. The shape of the CED block is approximately the following:
class C {
identifier {
writable?: boolean;
enumerable?: boolean;
configurable?: boolean;
value?: unknown;
get?: () => T;
set?: (v: T) => void;
}: T;
}
The value
property of the CED maps to the value defined on the prototype (for non-static CEDs). Essentially, the following two definitions have the same semantics:
class C {
m() {}
m { value() {} };
}
Unlike method syntax, however, value
can be assigned any value and it will still be assigned to the prototype:
class C {
m { value = 123 };
}
C.prototype.m; // 123
A common use case for meta-programming and decoration is to intercept access to a property and add functionality. This can be used for instance to add reactivity to a property. As part of this proposal, providing an empty get
or set
value will instead generate a default accessor which accesses a backing storage property, similar to auto-implemented properties in C#.
class C {
x { get; set; } = 123;
}
This syntax could be approximately implemented (e.g. polyfilled) like so:
class C {
#x = 123;
get x() {
return this.#x;
}
set x(v) {
this.#x = v;
}
}
This getter and setter can then be replaced (for instance via a decorator), while keeping the backing storage slot which contains the state of the field.
In order to avoid confusion, get
and set
are the only auto-implemented values. value
would require some value or implementation to be assigned to it, even if that value is undefined:
class C {
x { value; } // Syntax error: value must have a value assigned to it
x { value = undefined; } // Valid, makes C.prototype.x === undefined
}
CEDs can be used with fields or prototype values. Where the value exists depends on what values are included in the CED. Here are some examples of what ends up as a class field and what ends up as a method, and what combinations are invalid
class C {
// fields, on instance
x { writable = true };
x { enumerable = true };
x { configurable = true };
// fields with initializers, on instance
x { writable = true } = 123;
x { enumerable = true } = 123;
x { configurable = true } = 123;
// auto-accessors, on both instance and prototype
x { get; };
x { set; };
x { get; } = 123;
x { set; } = 123;
// methods, on prototype
x { value() {} };
x { get() {} };
x { set() {} };
x { get = someGetFn };
x { set = someSetFn };
// invalid combinations/syntax errors
x { value() {} } = 123; // Cannot have both a value and initializer
x { get() {} } = 123; // Can only have empty/auto get if you have initializer
x { set() {} } = 123; // Can only have empty/auto set if you have initializer
x { get = someGetFn } = 123; // Can only have empty/auto get if you have initializer
x { set = someSetFn } = 123; // Can only have empty/auto set if you have initializer
}
The proposed syntax would carve out an entire syntactic space (i.e., Identifier {...}
) that could prevent future exploration of syntax in this space. For instance, static {}
has already been added in this space (and would prevent a CED named static
from ever being defined), and its certainly possible that future extensions and features could also come up.
One way we could get around this is with a more explicit syntactic opt-in, either via a keyword before the CED or some alternative syntax for the CED block which distinguishes it. Some ideas:
class C {
// `define` keyword
define x { writable = false } = 123;
@reactive define x { get; set; } = 123;
// `def` keyword
def x { writable = false } = 123;
@reactive def x { get; set; } = 123;
}