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 and set 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 { ... })
  • A grouped get and set 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:
    class C {
      accessor y {
        get() { ... } 
        #set(value) { ... }
      }
    }
    
    Introduces a y getter on the prototype and a #y setter on the instance.
  • A decorator applied to the group could observe both the get and set 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 with get, 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
  1. Let name be the result of evaluting ClassElementName.
  2. ReturnIfAbrupt(name).
  3. Let initializer be a Function Object created in accordance with Step 3 of https://tc39.es/ecma262/#sec-runtime-semantics-classfielddefinitionevaluation.
  4. Return EvaluateAccessorGroup for AccessorGroup with arguments object, name, and initializer.
ClassElement : `accessor` ClassElementName AccessorGroup
  1. Let name be the result of evaluting ClassElementName.
  2. ReturnIfAbrupt(name).
  3. Return EvaluateAccessorGroup for AccessorGroup with arguments object, name, and empty.
ClassElement : `accessor` ClassElementName Initializer `;`
  1. Let name be the result of evaluting ClassElementName.
  2. ReturnIfAbrupt(name).
  3. Let initializer be a Function Object created in accordance with Step 3 of https://tc39.es/ecma262/#sec-runtime-semantics-classfielddefinitionevaluation.
  4. Return EvaluateAutoAccessor(object, name, initializer).
ClassElement : `accessor` ClassElementName `;`
  1. Let name be the result of evaluting ClassElementName.
  2. ReturnIfAbrupt(name).
  3. 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 : `{` `}`
  1. Return EvaluateAutoAccessor(object, name, initializer).
AccessorGroup : `{` GetAccessorStub SetAccessorStub? `}`
AccessorGroup : `{` SetAccessorStub GetAccessorStub? `}`
  1. Let list be a new empty List.
  2. Let backingFieldName be a unique Private Name (steps TBD).
  3. Let backingField be a new ClassFieldDefinition Record { [[Name]]: backingFieldName, [[Initializer]]: initializer }.
  4. If GetAccessorStub is present, then
    1. Let getAccessor be ! DefineAccessorStub(object, name, get, backingFieldName).
    2. If getAccessor is not empty, append getAccessor to list.
  5. If SetAccessorStub is present, then
    1. If SetAccessorStub is a PublicSetAccessorStub symbol, then:
      1. Let setAccessor be ! DefineAccessorStub(object, name, set, backingFieldName).
    2. Else,
      1. Let setAccessor be ! DefineAccessorStub(object, name, private-set, backingFieldName).
    3. If setAccessor is not empty, append setAccessor to list.
  6. return list.
AccessorGroup : `{` GetAccessorMethod SetAccessorMethod? `}`
AccessorGroup : `{` SetAccessorMethod GetAccessorMethod? `}`
  1. Assert: initializer is empty.
  2. Let list be a new empty List.
  3. If GetAccessorMethod is present, then
    1. Let getAccessor be ? DefineAccessorMethod of GetAccessorMethod with arguments object and name.
    2. If getAccessor is not empty, append getAccessor to list.
  4. If SetAccessorMethod is present, then
    1. Let setAccessor be ? DefineAccessorMethod of SetAccessorMethod with arguments object and name.
    2. If setAccessor is not empty, append setAccessor to list.
  5. Return list.

EvaluateAutoAccessor ( object, name, initializer )

  1. Let list be a new empty List.
  2. Let backingFieldName be a unique Private Name (steps TBD).
  3. Let backingField be a new ClassFieldDefinition Record { [[Name]]: backingFieldName, [[Initializer]]: initializer }.
  4. Let getAccessor be ! DefineAccessorStub(object, name, get, backingFieldName).
  5. Append getAccessor to list.
  6. Let setAccessor be ! DefineAccessorStub(object, name, set, backingFieldName).
  7. Append setAccessor to list.
  8. return list.

DefineAccessorStub ( object, name, kind, backingFieldName )

  1. If kind is get, then
    1. Return the result of defining a getter method on object named name that returns the value of backingFieldName (steps TBD).
  2. If kind is set, then
    1. Return the result of defining a setter method on object named name that returns the value of backingFieldName (steps TBD).
  3. If kind is private-set, then
    1. Assert: name is not a Private NAme.
    2. Let privateIdentifier be the string-concatenation of 0x0023 (NUMBER SIGN) and name.
    3. 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).
    4. 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 `}`
  1. Return the result of defining a getter method on object named name whose body is FunctionBody (steps TBD).
PublicSetAccessorMethod : `set` `(` PropertySetParameterList `)` `{` FunctionBody `}`
  1. 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 `}`
  1. Assert: name is not a Private Name.
  2. 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 individual get and set 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 and set 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

  • C# (1, 2)

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

Stage 3 Entrance Criteria

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.