Skip to content

Instantly share code, notes, and snippets.

@pzuraq
Created March 27, 2022 18:17

Revisions

  1. pzuraq renamed this gist Mar 27, 2022. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. pzuraq created this gist Mar 27, 2022.
    193 changes: 193 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,193 @@
    # 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`.

    ```js
    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](https://github.com/tc39/proposal-decorators#class-accessors).
    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](https://github.com/tc39/proposal-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:

    ```js
    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:

    ```ts
    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:

    ```js
    class C {
    m() {}

    m { value() {} };
    }
    ```

    Unlike method syntax, however, `value` can be assigned any value and it will still be assigned to the prototype:

    ```js
    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#](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties).

    ```js
    class C {
    x { get; set; } = 123;
    }
    ```

    This syntax could be approximately implemented (e.g. polyfilled) like so:

    ```js
    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:

    ```js
    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

    ```js
    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:

    ```js
    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;
    }
    ```