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.
Stage: 1
Champion: Ron Buckton (@rbuckton)
For detailed status of this proposal see TODO, below.
- Ron Buckton (@rbuckton)
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) { ... } } }
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.
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` `;`
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) }
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.
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).
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,
2. Let setAccessor be ! DefineAccessorStub(object, name,
private-set, backingFieldName). - 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.
- 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.
- 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).
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).
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 {}
.
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 = ...;
}
}
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;
}
}
}
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
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
- 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.
- Initial specification text.
- Transpiler support (Optional).
- Complete specification text.
- Designated reviewers have signed off on the current spec text.
- The ECMAScript editor has signed off on the current spec text.
- 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.