Grouped Accessors and Auto-Accessors for ECMAScript
This introduces an investigation into new syntax for grouped accessors to classes and object literals and auto-accessors to classes.
A grouped accessor is a single declaration that contains either or both both of the get
and set
methods for an accessor.
An auto-accessor is a simplified variant of a grouped accessor that elides the bodies of the get
and set
methods and
introduces a private backing field used by both the getter and setter.
Under Consideration: We may consider expanding auto-accessors to work on object literals in the future, however the necessary private name semantics are currently not defined for object literals.
Status
Stage: 1
Champion: Ron Buckton (@rbuckton)
For detailed status of this proposal see TODO, below.
Authors
- Ron Buckton (@rbuckton)
Grouped Accessors
class C {
accessor x {
get() { ... } // equivalent to `get x() { ... }`
set(value) { ... } // equivalent to `set x(value) { ... }`
}
accessor y {
get() { ... } // equivalent to `get y() { ... }`
#set(value) { ... } // equivalent to `set #y(value) { ... }`
}
accessor #z {
get() { ... } // equivalent to `get #z() { ... }`
set(value) { ... } // equivalent to `set #z(value) { ... }`
}
}
const obj = {
accessor x {
get() { ... }
set(value) { ... }
}
};
A grouped accessor is essentially a way to define either one or both of the get
and set
methods of an accessor in a
single logical group. This provides the following benefits:
- The
get
andset
declarations are logically grouped together, which improves readability.- This can also result in an improved editor experience in editors with support for folding (i.e.,
▶ accessor x { ... }
)
- This can also result in an improved editor experience in editors with support for folding (i.e.,
- A grouped
get
andset
accessor pair with a ComputedPropertyName only needs to have its name evaluated once. - In a
class
, the setter for a public property can be marked as private using#set
, this introduces both a public binding for the identifer, and a private binding for the identifier (but prefixed with#
). For example:
Introduces aclass C { accessor y { get() { ... } #set(value) { ... } } }
y
getter on the prototype and a#y
setter on the instance. - A decorator applied to the group could observe both the
get
andset
methods simultaneously for entangled operations. For example:function dec({ get, set }, context) { ... return { get, set, }; } class C { @dec accessor x { get() { ... } set(value) { ... } } }
Auto-Accessors
class C {
accessor a = 1; // same as `accessor a { get; set; } = 1;`
accessor b { } = 1; // same as `accessor b { get; set; } = 1;`
accessor c { get; set; } = 1; // same as `accessor c = 1;`
accessor d { get; } = 1; // getter but no setter
accessor e { set; } = 1; // setter but no getter (use case: decorators)
accessor f { get; #set; }; // getter with private setter `#f`;
accessor g { #set; } = 1; // private setter but no getter (use case: decorators)
accessor #h = 1; // same as `accessor #g { get; set; } = 1;`
accessor #i { } = 1; // same as `accessor #h { get; set; } = 1;`
accessor #j { get; set; } = 1; // same as `accessor #i = 1;`
accessor #k { get; } = 1; // getter but no setter
accessor #l { set; } = 1; // setter but no getter (use case: decorators)
// also allowed:
accessor "foo"; // same as `accessor "foo" { get; set; }`
accessor 1; // same as `accessor 1 { get; set; }`
accessor [x]; // same as `accessor [x] { get; set; }`
// not allowed:
// accessor "bar" { get; #set; } // error: no private setters for string properties
// accessor 2 { get; #set; } // error: no private setters for numeric properties
// accessor [y] { get; #set; } // error: no private setters for computed properties
// accessor #m { get; #set; }; // error: accessor is already private
// accessor #n { #set; }; // error: accessor is already private
}
An auto-accessor is a simplified version of a grouped accessor that allows you to elide the body of the get
and set
methods, and optionally provide an initializer. An auto-accessor introduces a unique unnamed private field on
the class which is wrapped by a generated getter and an optional generated setter. Using #set
instead of set
indicates that a
private setter of the same name as the public member (but prefixed with #
) exists on the object and provides privileged access to set the
underlying value.
This provides the following benefits:
- Introduces accessors that can be overridden in subclasses without excess boilerplate.
- Provides a replacement for fields that allows you to observe reading and writing the value of the field with decorators.
- Allows you to perform initialization inline with the declaration, similar to fields.
Proposed Syntax
ClassElement[Yield, Await] :
...
`accessor` ClassElementName[?Yield, ?Await] AccessorGroup Initializer[?Yield, ?Await] `;`
`accessor` ClassElementName[?Yield, ?Await] AccessorGroup
`accessor` ClassElementName[?Yield, ?Await] Initializer[?Yield, ?Await]? `;`
AccessorGroup :
`{` `}`
`{` GetAccessorMethodOrStub SetAccessorMethodOrStub? `}`
`{` SetAccessorMethodOrStub GetAccessorMethodOrStub? `}`
GetAccessorMethodOrStub :
GetAccessorMethod
GetAccessorStub
GetAccessorMethod :
`get` `(` `)` `{` FunctionBody[~Yield, ~Await] `}`
GetAccessorStub :
`get` `;`
SetAccessorMethodOrStub :
SetAccessorMethod
SetAccessorStub
SetAccessorMethod :
PublicSetAccessorMethod
PrivateSetAccessorMethod
PublicSetAccessorMethod :
`set` `(` PropertySetParameterList `)` `{` FunctionBody[~Yield, ~Await] `}`
PrivateSetAccessorMethod :
`#set` `(` PropertySetParameterList `)` `{` FunctionBody[~Yield, ~Await] `}`
SetAccessorStub :
PublicSetAccessorStub
PrivateSetAccessorStub
PublicSetAccessorStub :
`set` `;`
PrivateSetAccessorStub :
`#set` `;`
Proposed Semantics
The following represents some approximate semantics for this proposal. The gist of which is the following:
Only
accessor
properties with Identifier names can have a#set
stub or#set
method:class C { accessor x { get; #set; } // ok accessor #y { get; #set; } // syntax error accessor "z" { get; #set; } // syntax error accessor 1 { get; #set; } // syntax error accessor [expr] { get; #set; } // syntax error }
You cannot have both a
set
and a#set
in the same group:class C { accessor x { get; #set; } // ok accessor y { set; #set; } // syntax error }
You cannot combine
get
,set
, or#set
stub definitions withget
,set
, or#set
methods:class C { accessor x { get; #set; } // ok accessor y { get() { return 1; } set(v) { } } // ok accessor z { get() { return 1; } set; } // error }
You cannot combine
get
,set
, or#set
methods with an initializer:class C { accessor w = 1; // ok accessor x { get; } = 1; // ok accessor y { get; set; } = 1; // ok accessor z { get() { return 1; } } = 1; // error }
You cannot have an
accessor
property that has a#set
stub or method that collides with another private name on the class:class C { #w; accessor w; // ok #x; accessor x { get; set; }; // ok #y; accessor y { get; #set; }; // error (collides with #y) #z; accessor z { get() { } #set(v) { } }; // error (collides with #z) }
Early Errors
ClassElement : `accessor` ClassElementName AccessorGroup Initializer `;`
- It is a Syntax Error if AccessorGroup Contains GetAccessorMethod.
- It is a Syntax Error if AccessorGroup Contains SetAccessorMethod.
- It is a Syntax Error if ClassElementName is not Identifier and AccessorGroup Contains PrivateSetAccessorStub.
ClassElement : `accessor` ClassElementName AccessorGroup
- It is a Syntax Error if AccessorGroup Contains GetAccessorMethod and AccessorGroup Contains SetAccessorStub.
- It is a Syntax Error if AccessorGroup Contains SetAccessorMethod and AccessorGroup Contains GetAccessorStub.
- It is a Syntax Error if ClassElementName is not Identifier and AccessorGroup Contains PrivateSetAccessorStub.
- It is a Syntax Error if ClassElementName is not Identifier and AccessorGroup Contains PrivateSetAccessorMethod.
Under Consideration: We may choose to make it an early error to have both a grouped set
and a grouped #set
for the
same name on the same class.
ClassElementEvaluation
With parameter object.
ClassElement : `accessor` ClassElementName AccessorGroup Initializer
- Let name be the result of evaluting ClassElementName.
- ReturnIfAbrupt(name).
- Let initializer be a Function Object created in accordance with Step 3 of https://tc39.es/ecma262/#sec-runtime-semantics-classfielddefinitionevaluation.
- Return EvaluateAccessorGroup for AccessorGroup with arguments object, name, and initializer.
ClassElement : `accessor` ClassElementName AccessorGroup
- Let name be the result of evaluting ClassElementName.
- ReturnIfAbrupt(name).
- Return EvaluateAccessorGroup for AccessorGroup with arguments object, name, and
empty.
ClassElement : `accessor` ClassElementName Initializer `;`
- Let name be the result of evaluting ClassElementName.
- ReturnIfAbrupt(name).
- Let initializer be a Function Object created in accordance with Step 3 of https://tc39.es/ecma262/#sec-runtime-semantics-classfielddefinitionevaluation.
- Return EvaluateAutoAccessor(object, name, initializer).
ClassElement : `accessor` ClassElementName `;`
- Let name be the result of evaluting ClassElementName.
- ReturnIfAbrupt(name).
- Return EvaluateAutoAccessor(object, name,
empty).
EvaluateAccessorGroup
With parameters object, name, and initializer.
NOTE: The following semantics are approximate and will be specified in full at a later date.
AccessorGroup : `{` `}`
- Return EvaluateAutoAccessor(object, name, initializer).
AccessorGroup : `{` GetAccessorStub SetAccessorStub? `}`
AccessorGroup : `{` SetAccessorStub GetAccessorStub? `}`
- Let list be a new empty List.
- Let backingFieldName be a unique Private Name (steps TBD).
- Let backingField be a new ClassFieldDefinition Record { [[Name]]: backingFieldName, [[Initializer]]: initializer }.
- If GetAccessorStub is present, then
- Let getAccessor be ! DefineAccessorStub(object, name,
get, backingFieldName). - If getAccessor is not
empty, append getAccessor to list.
- Let getAccessor be ! DefineAccessorStub(object, name,
- If SetAccessorStub is present, then
- If SetAccessorStub is a PublicSetAccessorStub symbol, then:
- Let setAccessor be ! DefineAccessorStub(object, name,
set, backingFieldName).
- Let setAccessor be ! DefineAccessorStub(object, name,
- Else,
- Let setAccessor be ! DefineAccessorStub(object, name,
private-set, backingFieldName).
- Let setAccessor be ! DefineAccessorStub(object, name,
- If setAccessor is not
empty, append setAccessor to list.
- If SetAccessorStub is a PublicSetAccessorStub symbol, then:
- return list.
AccessorGroup : `{` GetAccessorMethod SetAccessorMethod? `}`
AccessorGroup : `{` SetAccessorMethod GetAccessorMethod? `}`
- Assert: initializer is
empty. - Let list be a new empty List.
- If GetAccessorMethod is present, then
- Let getAccessor be ? DefineAccessorMethod of GetAccessorMethod with arguments object and name.
- If getAccessor is not
empty, append getAccessor to list.
- If SetAccessorMethod is present, then
- Let setAccessor be ? DefineAccessorMethod of SetAccessorMethod with arguments object and name.
- If setAccessor is not
empty, append setAccessor to list.
- Return list.
EvaluateAutoAccessor ( object, name, initializer )
- Let list be a new empty List.
- Let backingFieldName be a unique Private Name (steps TBD).
- Let backingField be a new ClassFieldDefinition Record { [[Name]]: backingFieldName, [[Initializer]]: initializer }.
- Let getAccessor be ! DefineAccessorStub(object, name,
get, backingFieldName). - Append getAccessor to list.
- Let setAccessor be ! DefineAccessorStub(object, name,
set, backingFieldName). - Append setAccessor to list.
- return list.
DefineAccessorStub ( object, name, kind, backingFieldName )
- If kind is
get, then- Return the result of defining a getter method on object named name that returns the value of backingFieldName (steps TBD).
- If kind is
set, then- Return the result of defining a setter method on object named name that returns the value of backingFieldName (steps TBD).
- If kind is
private-set, then- Assert: name is not a Private NAme.
- Let privateIdentifier be the string-concatenation of 0x0023 (NUMBER SIGN) and name.
- Let privateName be a Private Name for privateIdentifier, similar to the steps for
ClassElementName : PrivateIdentifier
in https://tc39.es/ecma262/#sec-class-definitions-runtime-semantics-evaluation (steps TBD). - Return the result of defining a setter method on object named name that returns the value of backingFieldName (steps TBD).
DefineAccessorMethod
With arguments object and name.
GetAccessorMethod : `get` `(` `)` `{` FunctionBody `}`
- Return the result of defining a getter method on object named name whose body is FunctionBody (steps TBD).
PublicSetAccessorMethod : `set` `(` PropertySetParameterList `)` `{` FunctionBody `}`
- Return the result of defining a setter method on object named name with parameters PropertySetParameterList and whose body is FunctionBody (steps TBD).
PrivateSetAccessorMethod : `#set` `(` PropertySetParameterList `)` `{` FunctionBody `}`
- Assert: name is not a Private Name.
- Return the result of defining a setter method on object named name with parameters PropertySetParameterList and whose body is FunctionBody (steps TBD).
Interaction With Class static {}
Initialization Block
The initial proposal for this feature did not use the accessor
keyword prefix to distinguish a grouped- or auto-accessor,
which lead to a collision with the Class static {}
Initialization Block proposal.
The current version now requires the accessor
keyword and no longer conflicts with static {}
.
Interaction with Decorators
This proposal is intended to dovetail with the Decorators proposal and shares syntax with auto-accessors in that proposal. This proposal expands upon the Decorators proposal in the following ways:
By adding an AccessorGroup to an auto-accessor, you are able to decorate both the entire
accessor
declaration as well as the individualget
andset
method stubs:class C { @dec1 // called as `dec1({ get, set }, context)` accessor x { @dec2 // called as `dec2(fn, context)` get; @dec3 // called as `dec3(fn, context)` set; } }
A decorator on a grouped accessor is able to access both the
get
andset
declarations, similar to early decorator implementations in TypeScript and Babel:class C { @dec1 // called as `dec1({ get, set }, context)` accessor x { get() { ... } set(v) { ... } } @dec2 get y() { ... } // called as `dec2(fn, context)` }
Similar to auto-accessors, grouped accessors can be decorated both at the
accessor
declaration level and at the individual getter and setter declarations:class C { @dec1 // called as `dec1({ get, set }, context)` accessor x { @dec2 // called as `dec2(fn, context)` get() { ... } @dec3 // called as `dec3(fn, context)` set(v) { ... } } }
In addition, some aspects of auto-accessors that may at first seem like edge cases become powerful capabilities with decorators:
// Get-only accessors
// an accessor with only a getter and no initializer is has limited use on its own...
class Service {
accessor users { get; }
}
// ...however a decorator could be used to perform dependency injection by replacing the getter:
class Service {
@inject("userService")
accessor users { get; }
}
// Set-only accessors
// A setter with no getter is has limited use on its own...
class WriteOnlyCounter {
accessor inc { set; }
}
// ...however, a decorator can replace the setter to make it more useful:
class WriteOnlyCounter {
@observeChanges()
accessor inc { set; }
}
// Private-set-only accessors
// The following could have been written as `accessor #value { set; }`...
class Widget {
accessor value { #set; }
exec() {
this.#value = ...;
}
}
// ...however, a decorator here could attach a public getter, which
// it would not be able to do if `value` was named `#value` instead.
class Widget {
@decorator
accessor value { #set; }
exec() {
this.#value = ...;
}
}
Prior Art
Examples
Grouped Accessors
class Point {
#x = 0;
#y = 0;
accessor x {
get() { return this.#x; }
set(v) {
if (typeof v !== "number") throw new RangeError();
this.#x = v;
}
}
accessor y {
get() { return this.#y; }
set(v) {
if (typeof v !== "number") throw new RangeError();
this.#y = v;
}
}
}
Auto-Accessors
class Customer {
accessor id { get; #set; } // public get, private set
accessor name;
constructor(id, name) {
this.#id = id;
this.name = name;
}
}
const c = new Customer(1, "Jane");
c.id; // 1
c.id = 2; // TypeError
TODO
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
Stage 1 Entrance Criteria
- Identified a "champion" who will advance the addition.
- Prose outlining the problem or need and the general shape of a solution.
- Illustrative examples of usage.
-
High-level API.
Stage 2 Entrance Criteria
- Initial specification text.
- Transpiler support (Optional).
Stage 3 Entrance Criteria
- Complete specification text.
- Designated reviewers have signed off on the current spec text.
- The ECMAScript editor has signed off on the current spec text.
Stage 4 Entrance Criteria
- Test262 acceptance tests have been written for mainline usage scenarios and merged.
- Two compatible implementations which pass the acceptance tests: [1], [2].
- A pull request has been sent to tc39/ecma262 with the integrated spec text.
- The ECMAScript editor has signed off on the pull request.