Skip to content

Instantly share code, notes, and snippets.

@pzuraq
Created March 27, 2022 18:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pzuraq/ae002a0f3e8745b57cd6c046bf9ff89f to your computer and use it in GitHub Desktop.
Save pzuraq/ae002a0f3e8745b57cd6c046bf9ff89f to your computer and use it in GitHub Desktop.
Class Element Definitions

Class Element Definitions

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:

  1. Changing the enumerable, writable, and configurable properties of class elements in a declarative manner.
  2. Grouping related element definitions, such as getters and setters, in a single location.
  3. Automatic definitions for getters and setters, which simplifies common decoration use cases.
  4. Definition of non-method values on class prototypes.

Motivation

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.

Detailed Design

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 field
  • enumerable - MUST be a class field
  • configurable - MUST be a class field
  • value - can be a class field or a class method. CANNOT be an empty field.
  • get - can be a class field or a class method
  • set - 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;
}

Defining prototype values

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

Auto-Accessors

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
}

Valid and Invalid Combinations

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
}

Alternatives

Syntactic Opt-In

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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment