Git Product home page Git Product logo

proposal-decorators's Introduction

Decorators

Stage: 3

Decorators are a proposal for extending JavaScript classes which is widely adopted among developers in transpiler environments, with broad interest in standardization. TC39 has been iterating on decorators proposals for over five years. This document describes a new proposal for decorators based on elements from all past proposals.

This README describes the current decorators proposal, which is a work in progress. For previous iterations of this proposal, see the commit history of this repository.

Introduction

Decorators are functions called on classes, class elements, or other JavaScript syntax forms during definition.

@defineElement("my-class")
class C extends HTMLElement {
  @reactive accessor clicked = false;
}

Decorators have three primary capabilities:

  1. They can replace the value that is being decorated with a matching value that has the same semantics. (e.g. a decorator can replace a method with another method, a field with another field, a class with another class, and so on).
  2. They can provide access to the value that is being decorated via accessor functions which they can then choose to share.
  3. They can initialize the value that is being decorated, running additional code after the value has been fully defined. In cases where the value is a member of class, then initialization occurs once per instance.

Essentially, decorators can be used to metaprogram and add functionality to a value, without fundamentally changing its external behavior.

This proposal differs from previous iterations where decorators could replace the decorated value with a completely different type of value. The requirement for decorators to only replace a value with one that has the same semantics as the original value fulfills two major design goals:

  • It should be easy both to use decorators and to write your own decorators. Previous iterations such as the static decorators proposal were complicated for authors and implementers in particular. In this proposal, decorators are plain functions, and are accessible and easy to write.
  • Decorators should affect the thing they're decorating, and avoid confusing/non-local effects. Previously, decorators could change the decorated value in unpredictable ways, and also add completely new values which were unrelated. This was problematic both for runtimes, since it meant decorated values could not be analyzed statically, and for developers, since decorated values could turn into completely different types of values without any indicator to the user.

In this proposal, decorators can be applied to the following existing types of values:

  • Classes
  • Class fields (public, private, and static)
  • Class methods (public, private, and static)
  • Class accessors (public, private, and static)

In addition, this proposal introduces a new type of class element that can be decorated:

  • Class auto accessors, defined by applying the accessor keyword to a class field. These have a getter and setter, unlike fields, which default to getting and setting the value on a private storage slot (equivalent to a private class field):

    class Example {
      @reactive accessor myBool = false;
    }

This new element type can be used independently, and has its own semantics separate from usage with decorators. The reason it is included in this proposal is primarily because there are a number of use cases for decorators which require its semantics, since decorators can only replace an element with a corresponding element that has the same semantics. These use cases are common in the existing decorators ecosystem, demonstrating a need for the capabilities they provide.

Motivation

You might be wondering "why do we need these at all?" Decorators are a powerful metaprogramming feature that can simplify code significantly, but can also feel "magical" in the sense that they hide details from the user, making what's going on under the hood harder to understand. Like all abstractions, in some cases decorators can become more trouble than they're worth.

However, one of the main reasons decorators are still being pursued today, and specifically the main reason class decorators are an important language feature, is that they fill a gap that exists in the ability to metaprogram in JavaScript.

Consider the following functions:

function logResult(fn) {
  return function(...args) {
    try {
      const result = fn.call(this, ...args);
      console.log(result);
    } catch (e) {
      console.error(result);
      throw e;
    }
    return result;
  }
}

const plusOne = logResult((x) => x + 1);

plusOne(1); // 2

This is a common pattern used in JavaScript every day, and is a fundamental power in languages that support closures. This is an example of implementing the decorator pattern in plain JavaScript. You can use logResult to add logging to any function definition easily, and you can do this with any number of "decorator" functions:

const foo = bar(baz(qux(() => /* do something cool */)))

In some other languages, like Python, decorators are syntactic sugar for this pattern - they're functions that can be applied to other functions with the @ symbol, or by calling them directly, to add additional behavior.

So, as it stands today, it is possible to use the decorator pattern in JavaScript when it comes to functions, just without the nice @ syntax. This pattern is also declarative, which is important - there is no step between the definition of the function and decoration of it. This means it's not possible for someone to accidentally use the undecorated version of the function, which could cause major bugs and make it very difficult to debug!

However, there is a place where we can't use this pattern at all - objects and classes. Consider the following class:

class MyClass {
  x = 0;
}

How would we go about adding the logging functionality to x, so that whenever we get or set it, we log that access? You could do it manually:

class MyClass {
  #x = 0;

  get x() {
    console.log('getting x');
    return this.#x;
  }

  set x(v) {
    console.log('setting x');
    this.#x = v;
  }
}

But if we're doing this a lot, it would be a pain to add all those getters and setters everywhere. We could make a helper function to do it for us after we define the class:

function logResult(Class, property) {
  Object.defineProperty(Class.prototype, property, {
    get() {
      console.log(`getting ${property}`);
      return this[`_${property}`];
    },

    set(v) {
      console.log(`setting ${property}`);
      this[`_${property}`] = v;
    }
  })
}

class MyClass {
  constructor() {
    this.x = 0;
  }
}

logResult(MyClass, 'x');

This works, but if we use a class field it would overwrite the getter/setter we defined on the prototype, so we have to move the assignment to the constructor. It's also done in multiple statements, so the definition itself happens over time and is not declarative. Imagine debugging a class that is "defined" in multiple files, each one adding different decorations as your application boots up. That may sound like a really bad design, but it was not uncommon in the past before classes were introduced! Lastly, there's no way for us to do this with private fields or methods. We can't just replace the definition.

Methods are a little better, we could do something like this:

function logResult(fn) {
  return function(...args) {
    const result = fn.call(this, ...args);
    console.log(result);
    return result;
  }
}

class MyClass {
  x = 0;
  plusOne = logResult(() => this.x + 1);
}

While this is declarative, it also creates a new closure for each instance of the class, which is a lot of additional overhead at scale.

By making class decorators a language feature, we are plugging this gap and enabling the decorator pattern for class methods, fields, accessors, and classes themselves. This allows developers to easily write abstractions for common tasks, such as debug logging, reactive programming, dynamic type checking, and more.

Detailed Design

The three steps of decorator evaluation:

  1. Decorator expressions (the thing after the @) are evaluated interspersed with computed property names.
  2. Decorators are called (as functions) during class definition, after the methods have been evaluated but before the constructor and prototype have been put together.
  3. Decorators are applied (mutating the constructor and prototype) all at once, after all of them have been called.

The semantics here generally follow the consensus at the May 2016 TC39 meeting in Munich.

1. Evaluating decorators

Decorators are evaluated as expressions, being ordered along with computed property names. This goes left to right, top to bottom. The result of decorators is stored in the equivalent of local variables to be later called after the class definition initially finishes executing.

2. Calling decorators

When decorators are called, they receive two parameters:

  1. The value being decorated, or undefined in the case of class fields which are a special case.
  2. A context object containing information about the value being decorated

Using TypeScript interfaces for brevity and clarity, this is the general shape of the API:

type Decorator = (value: Input, context: {
  kind: string;
  name: string | symbol;
  access: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  private?: boolean;
  static?: boolean;
  addInitializer(initializer: () => void): void;
}) => Output | void;

Input and Output here represent the values passed to and returned from a given decorator. Each type of decorator has a different input and output, and these are covered below in more detail. All decorators can choose to return nothing, which defaults to using the original, undecorated value.

The context object also varies depending on the value being decorated. Breaking down the properties:

  • kind: The kind of decorated value. This can be used to assert that the decorator is used correctly, or to have different behavior for different types of values. It is one of the following values.
    • "class"
    • "method"
    • "getter"
    • "setter"
    • "field"
    • "accessor"
  • name: The name of the value, or in the case of private elements the description of it (e.g. the readable name).
  • access: An object containing methods to access the value. These methods also get the final value of the element on the instance, not the current value passed to the decorator. This is important for most use cases involving access, such as type validators or serializers. See the section on Access below for more details.
  • static: Whether or not the value is a static class element. Only applies to class elements.
  • private: Whether or not the value is a private class element. Only applies to class elements.
  • addInitializer: Allows the user to add additional initialization logic to the element or class.

See the Decorator APIs section below for a detailed breakdown of each type of decorator and how it is applied.

3. Applying decorators

Decorators are applied after all decorators have been called. The intermediate steps of the decorator application algorithm are not observable--the newly constructed class is not made available until after all method and non-static field decorators have been applied.

The class decorator is called only after all method and field decorators are called and applied.

Finally, static fields are executed and applied.

Syntax

This decorators proposal uses the syntax of the previous Stage 2 decorators proposal. This means that:

  • Decorator expressions are restricted to a chain of variables, property access with . but not [], and calls (). To use an arbitrary expression as a decorator, @(expression) is an escape hatch.
  • Class expressions may be decorated, not just class declarations.
  • Class decorators may exclusively come before, or after, export/export default.

There is no special syntax for defining decorators; any function can be applied as a decorator.

Decorator APIs

Class Methods

type ClassMethodDecorator = (value: Function, context: {
  kind: "method";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

Class method decorators receive the method that is being decorated as the first value, and can optionally return a new method to replace it. If a new method is returned, it will replace the original on the prototype (or on the class itself in the case of static methods). If any other type of value is returned, an error will be thrown.

An example of a method decorator is the @logged decorator. This decorator receives the original function, and returns a new function that wraps the original and logs before and after it is called.

function logged(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`ending ${name}`);
      return ret;
    };
  }
}

class C {
  @logged
  m(arg) {}
}

new C().m(1);
// starting m with arguments 1
// ending m

This example roughly "desugars" to the following (i.e., could be transpiled as such):

class C {
  m(arg) {}
}

C.prototype.m = logged(C.prototype.m, {
  kind: "method",
  name: "m",
  static: false,
  private: false,
}) ?? C.prototype.m;

Class Accessors

type ClassGetterDecorator = (value: Function, context: {
  kind: "getter";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

type ClassSetterDecorator = (value: Function, context: {
  kind: "setter";
  name: string | symbol;
  access: { set(value: unknown): void };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

Accessor decorators receive the original underlying getter/setter function as the first value, and can optionally return a new getter/setter function to replace it. Like method decorators, this new function is placed on the prototype in place of the original (or on the class for static accessors), and if any other type of value is returned, an error will be thrown.

Accessor decorators are applied separately to getters and setters. In the following example, @foo is applied only to get x() - set x() is undecorated:

class C {
  @foo
  get x() {
    // ...
  }

  set x(val) {
    // ...
  }
}

We can extend the @logged decorator we defined previously for methods to also handle accessors. The code is essentially the same, we just need to handle additional kinds.

function logged(value, { kind, name }) {
  if (kind === "method" || kind === "getter" || kind === "setter") {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`ending ${name}`);
      return ret;
    };
  }
}

class C {
  @logged
  set x(arg) {}
}

new C().x = 1
// starting x with arguments 1
// ending x

This example roughly "desugars" to the following (i.e., could be transpiled as such):

class C {
  set x(arg) {}
}

let { set } = Object.getOwnPropertyDescriptor(C.prototype, "x");
set = logged(set, {
  kind: "setter",
  name: "x",
  static: false,
  private: false,
}) ?? set;

Object.defineProperty(C.prototype, "x", { set });

Class Fields

type ClassFieldDecorator = (value: undefined, context: {
  kind: "field";
  name: string | symbol;
  access: { get(): unknown, set(value: unknown): void };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => (initialValue: unknown) => unknown | void;

Unlike methods and accessors, class fields do not have a direct input value when being decorated. Instead, users can optionally return an initializer function which runs when the field is assigned, receiving the initial value of the field and returning a new initial value. If any other type of value besides a function is returned, an error will be thrown.

We can expand our @logged decorator to be able to handle class fields as well, logging when the field is assigned and what the value is.

function logged(value, { kind, name }) {
  if (kind === "field") {
    return function (initialValue) {
      console.log(`initializing ${name} with value ${initialValue}`);
      return initialValue;
    };
  }

  // ...
}

class C {
  @logged x = 1;
}

new C();
// initializing x with value 1

This example roughly "desugars" to the following (i.e., could be transpiled as such):

let initializeX = logged(undefined, {
  kind: "field",
  name: "x",
  static: false,
  private: false,
}) ?? (initialValue) => initialValue;

class C {
  x = initializeX.call(this, 1);
}

The initializer function is called with the instance of the class as this, so field decorators can also be used to bootstrap registration relationships. For instance, you could register children on a parent class:

const CHILDREN = new WeakMap();

function registerChild(parent, child) {
  let children = CHILDREN.get(parent);

  if (children === undefined) {
    children = [];
    CHILDREN.set(parent, children);
  }

  children.push(child);
}

function getChildren(parent) {
  return CHILDREN.get(parent);
}

function register() {
  return function(value) {
    registerChild(this, value);

    return value;
  }
}

class Child {}
class OtherChild {}

class Parent {
  @register child1 = new Child();
  @register child2 = new OtherChild();
}

let parent = new Parent();
getChildren(parent); // [Child, OtherChild]

Classes

type ClassDecorator = (value: Function, context: {
  kind: "class";
  name: string | undefined;
  addInitializer(initializer: () => void): void;
}) => Function | void;

Class decorators receive the class that is being decorated as the first parameter, and may optionally return a new callable (a class, function, or Proxy) to replace it. If a non-callable value is returned, then an error is thrown.

We can further extend our @logged decorator to log whenever an instance of a class is created:

function logged(value, { kind, name }) {
  if (kind === "class") {
    return class extends value {
      constructor(...args) {
        super(...args);
        console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`);
      }
    }
  }

  // ...
}

@logged
class C {}

new C(1);
// constructing an instance of C with arguments 1

This example roughly "desugars" to the following (i.e., could be transpiled as such):

class C {}

C = logged(C, {
  kind: "class",
  name: "C",
}) ?? C;

new C(1);

If the class being decorated is an anonymous class, then the name property of the context object is undefined.

New Class Elements

Class Auto-Accessors

Class auto-accessors are a new construct, defined by adding the accessor keyword in front of a class field:

class C {
  accessor x = 1;
}

Auto-accessors, unlike regular fields, define a getter and setter on the class prototype. This getter and setter default to getting and setting a value on a private slot. The above roughly desugars to:

class C {
  #x = 1;

  get x() {
    return this.#x;
  }

  set x(val) {
    this.#x = val;
  }
}

Both static and private auto-accessors can be defined as well:

class C {
  static accessor x = 1;
  accessor #y = 2;
}

Auto-accessors can be decorated, and auto-accessor decorators have the following signature:

type ClassAutoAccessorDecorator = (
  value: {
    get: () => unknown;
    set(value: unknown) => void;
  },
  context: {
    kind: "accessor";
    name: string | symbol;
    access: { get(): unknown, set(value: unknown): void };
    static: boolean;
    private: boolean;
    addInitializer(initializer: () => void): void;
  }
) => {
  get?: () => unknown;
  set?: (value: unknown) => void;
  init?: (initialValue: unknown) => unknown;
} | void;

Unlike field decorators, auto-accessor decorators receive a value, which is an object containing the get and set accessors defined on the prototype of the class (or the class itself in the case of static auto-accessors). The decorator can then wrap these and return a new get and/or set, allowing access to the property to be intercepted by the decorator. This is a capability that is not possible with fields, but is possible with auto-accessors. In addition, auto-accessors can return an init function, which can be used to change the initial value of the backing value in the private slot, similar to field decorators. If an object is returned but any of the values are omitted, then the default behavior for the omitted values is to use the original behavior. If any other type of value besides an object containing these properties is returned, an error will be thrown.

Further extending the @logged decorator, we can make it handle auto-accessors as well, logging when the auto-accessor is initialized and whenever it is accessed:

function logged(value, { kind, name }) {
  if (kind === "accessor") {
    let { get, set } = value;

    return {
      get() {
        console.log(`getting ${name}`);

        return get.call(this);
      },

      set(val) {
        console.log(`setting ${name} to ${val}`);

        return set.call(this, val);
      },

      init(initialValue) {
        console.log(`initializing ${name} with value ${initialValue}`);
        return initialValue;
      }
    };
  }

  // ...
}

class C {
  @logged accessor x = 1;
}

let c = new C();
// initializing x with value 1
c.x;
// getting x
c.x = 123;
// setting x to 123

This example roughly "desugars" to the following:

class C {
  #x = initializeX.call(this, 1);

  get x() {
    return this.#x;
  }

  set x(val) {
    this.#x = val;
  }
}

let { get: oldGet, set: oldSet } = Object.getOwnPropertyDescriptor(C.prototype, "x");

let {
  get: newGet = oldGet,
  set: newSet = oldSet,
  init: initializeX = (initialValue) => initialValue
} = logged(
  { get: oldGet, set: oldSet },
  {
    kind: "accessor",
    name: "x",
    static: false,
    private: false,
  }
) ?? {};

Object.defineProperty(C.prototype, "x", { get: newGet, set: newSet });

Adding initialization logic with addInitializer

The addInitializer method is available on the context object that is provided to the decorator for every type of value. This method can be called to associate an initializer function with the class or class element, which can be used to run arbitrary code after the value has been defined in order to finish setting it up. The timing of these initializers depends on the type of decorator:

  • Class decorators: After the class has been fully defined, and after class static fields have been assigned.
  • Class static elements
    • Method and Getter/Setter decorators: During class definition, after static class methods have been assigned, before any static class fields are initialized
    • Field and Accessor decorators: During class definition, immediately after the field or accessor that they were applied to is initialized
  • Class non-static elements
    • Method and Getter/Setter decorators: During class construction, before any class fields are initialized
    • Field and Accessor decorators: During class construction, immediately after the field or accessor that they were applied to is initialized

Example: @customElement

We can use addInitializer with class decorators in order to create a decorator which registers a web component in the browser.

function customElement(name) {
  return (value, { addInitializer }) => {
    addInitializer(function() {
      customElements.define(name, this);
    });
  }
}

@customElement('my-element')
class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['some', 'attrs'];
  }
}

This example roughly "desugars" to the following (i.e., could be transpiled as such):

class MyElement {
  static get observedAttributes() {
    return ['some', 'attrs'];
  }
}

let initializersForMyElement = [];

MyElement = customElement('my-element')(MyElement, {
  kind: "class",
  name: "MyElement",
  addInitializer(fn) {
    initializersForMyElement.push(fn);
  },
}) ?? MyElement;

for (let initializer of initializersForMyElement) {
  initializer.call(MyElement);
}

Example: @bound

We could also use addInitializer with method decorators to create a @bound decorator, which binds the method to the instance of the class:

function bound(value, { name, addInitializer }) {
  addInitializer(function () {
    this[name] = this[name].bind(this);
  });
}

class C {
  message = "hello!";

  @bound
  m() {
    console.log(this.message);
  }
}

let { m } = new C();

m(); // hello!

This example roughly "desugars" to the following:

class C {
  constructor() {
    for (let initializer of initializersForM) {
      initializer.call(this);
    }

    this.message = "hello!";
  }

  m() {}
}

let initializersForM = []

C.prototype.m = bound(
  C.prototype.m,
  {
    kind: "method",
    name: "m",
    static: false,
    private: false,
    addInitializer(fn) {
      initializersForM.push(fn);
    },
  }
) ?? C.prototype.m;

Access and Metadata Sidechanneling

So far we've seen how decorators can be used to replace a value, but we haven't seen how the access object for the decorator can be used. Here's an example of dependency injection decorators which use this object via a metadata sidechannel to inject values on an instance.

const INJECTIONS = new WeakMap();

function createInjections() {
  const injections = [];

  function injectable(Class) {
    INJECTIONS.set(Class, injections);
  }

  function inject(injectionKey) {
    return function applyInjection(v, context) {
      injections.push({ injectionKey, set: context.access.set });
    };
  }

  return { injectable, inject };
}

class Container {
  registry = new Map();

  register(injectionKey, value) {
    this.registry.set(injectionKey, value);
  }

  lookup(injectionKey) {
    this.registry.get(injectionKey);
  }

  create(Class) {
    let instance = new Class();

    for (const { injectionKey, set } of INJECTIONS.get(Class) || []) {
      set.call(instance, this.lookup(injectionKey));
    }

    return instance;
  }
}

class Store {}

const { injectable, inject } = createInjections();

@injectable
class C {
  @inject('store') store;
}

let container = new Container();
let store = new Store();

container.register('store', store);

let c = container.create(C);

c.store === store; // true

Access is generally provided based on whether or not the value is a value meant to be read or written. Fields and auto-accessors can be both read and written to. Accessors can either be read in the case of getters, or written in the case of setters. Methods can only be read.

Possible extensions

Decorators on further constructs are investigated in EXTENSIONS.md.

Standardization plan

  • Iterate on open questions within the proposal, presenting them to TC39 and discussing further in the biweekly decorators calls, to bring a conclusion to committee in a future meeting
    • STATUS: Open questions have been resolved, decorators working group has reached general consensus on the design.
  • Write spec text
    • STATUS: Complete, available here.
  • Implement in experimental transpilers
    • STATUS: An experimental implementation has been created and is available for general use. Work is ongoing to implement in Babel and get more feedback.
  • Collect feedback from JavaScript developers testing the transpiler implementation
  • Propose for Stage 3.

FAQ

How should I use decorators in transpilers today?

Since decorators have reached stage 3 and are approaching completion, it is now recommended that new projects use the latest transforms for stage 3 decorators. These are available in Babel, TypeScript, and other popular build tools.

Existing projects should begin to develop upgrade plans for their ecosystems. In the majority of cases it should be possible to support both the legacy and stage 3 versions at the same time by matching on the arguments that are passed to the decorator. In a small number of cases this may not be possible due to a difference in the capabilities between the two versions. If you run into such a case, please open an issue on this repo for discussion!

How does this proposal compare to other versions of decorators?

Comparison with Babel "legacy" decorators

Babel legacy-mode decorators are based on the state of the JavaScript decorators proposal as of 2014. In addition to the syntax changes listed above, the calling convention of Babel legacy decorators differs from this proposal:

  • Legacy decorators are called with the "target" (the class or prototype under construction), whereas the class under construction is not made available to decorators in this proposal.
  • Legacy decorators are called with a full property descriptor, whereas this proposal calls decorators with just "the thing being decorated" and a context object. This means, for example, that it is impossible to change property attributes, and that getters and setters are not "coalesced" but rather decorated separately.

Despite these differences, it should generally be possible to achieve the same sort of functionality with this decorators proposal as with Babel legacy decorators. If you see important missing functionality in this proposal, please file an issue.

Comparison with TypeScript "experimental" decorators

TypeScript experimental decorators are largely similar to Babel legacy decorators, so the comments in that section apply as well. In addition:

  • This proposal does not include parameter decorators, but they may be provided by future built-in decorators, see EXTENSIONS.md.
  • TypeScript decorators run all instance decorators before all static decorators, whereas the order of evaluation in this proposal is based on the ordering in the program, regardless of whether they are static or instance.

Despite these differences, it should generally be possible to achieve the same sort of functionality with this decorators proposal as with TypeScript experimental decorators. If you see important missing functionality in this proposal, please file an issue.

Comparison with the previous Stage 2 decorators proposal

The previous Stage 2 decorators proposal was more full-featured than this proposal, including:

  • The ability of all decorators to add arbitrary 'extra' class elements, rather than just wrapping/changing the element being decorated.
  • Ability to declare new private fields, including reusing a private name in multiple classes
  • Class decorator access to manipulating all fields and methods within the class
  • More flexible handling of the initializer, treating it as a "thunk"

The previous Stage 2 decorators proposal was based on a concept of descriptors which stand in for various class elements. Such descriptors do not exist in this proposal. However, those descriptors gave a bit too much flexibility/dynamism to the class shape in order to be efficiently optimizable.

This decorators proposal deliberately omits these features, in order to keep the meaning of decorators "well-scoped" and intuitive, and to simplify implementations, both in transpilers and native engines.

Comparison with the "static decorators" proposal

Static decorators were an idea to include a set of built-in decorators, and support user-defined decorators derived from them. Static decorators were in a separate namespace, to support static analyzability.

The static decorators proposal suffered from both excessive complexity and insufficient optimizability. This proposal avoids that complexity by returning to the common model of decorators being ordinary functions.

See V8's analysis of decorator optimizability for more information on the lack of optimizability of the static decorators proposal, which this proposal aims to address.

If the previous TC39 decorators proposals didn't work out, why not go back and standardize TS/Babel legacy decorators?

Optimizability: This decorator proposal and legacy decorators are common in decorators being functions. However, the calling convention of this proposal is designed to be more optimizable by engines by making the following changes vs legacy decorators:

  • The incomplete class under construction is not exposed to decorators, so it does not need to observably undergo shape changes during class definition evaluation.
  • Only the construct being decorated may be changed in its contents; the "shape" of the property descriptor may not change.

Incompatibility with [[Define]] field semantics: Legacy decorators, when applied to field declarations, depend deeply on the semantics that field initializers call setters. TC39 concluded that, instead, field declarations act like Object.defineProperty. This decision makes many patterns with legacy decorators no longer work. Although Babel provides a way to work through this by making the initializer available as a thunk, these semantics have been rejected by implementers as adding runtime cost.

Why prioritize the features of "legacy" decorators, like classes, over other features that decorators could provide?

"Legacy" decorators have grown to huge popularity in the JavaScript ecosystem. That proves that they were onto something, and solve a problem that many people are facing. This proposal takes that knowledge and runs with it, building in native support in the JavaScript language. It does so in a way that leaves open the opportunity to use the same syntax for many more different kinds of extensions in the future, as described in EXTENSIONS.md.

Could we support decorating objects, parameters, blocks, functions, etc?

Yes! Once we have validated this core approach, the authors of this proposal plan to come back and make proposals for more kinds of decorators. In particular, given the popularity of TypeScript parameter decorators, we are considering including parameter decorators in this proposal's initial version. See EXTENSIONS.md.

Will decorators let you access private fields and methods?

Yes, private fields and methods can be decorated just like ordinary fields and methods. The only difference is that the name key on the context object is only a description of the element, not something we can be used to access it. Instead, an access object with get/set functions is provided. See the example under the heading, "Access".

How should this new proposal be used in transpilers, when it's implemented?

This decorators proposal would require a separate transpiler implementation from the previous legacy/experimental decorator semantics. The semantics could be switched into with a build-time option (e.g., a command-line flag or entry in a configuration file). Note that this proposal is expected to continue to undergo significant changes prior to Stage 3, and it should not be counted on for stability.

Modules exporting decorators are able to easily check whether they are being invoked in the legacy/experimental way or in the way described in this proposal, by checking whether their second argument is an object (in this proposal, always yes; previously, always no). So it should be possible to maintain decorator libraries which work with both approaches.

What makes this decorators proposal more statically analyzable than previous proposals? Is this proposal still statically analyzable even though it is based on runtime values?

In this decorators proposal, each decorator position has a consistent effect on the shape of the code generated after desugaring. No calls to Object.defineProperty with dynamic values for property attributes are made by the system, and it is also impractical to make these sorts of calls from user-defined decorators as the "target" is not provided to decorators; only the actual contents of the functions.

How does static analyzability help transpilers and other tooling?

Statically analyzable decorators help tooling to generate faster and smaller JavaScript from build tools, enabling the decorators to be transpiled away, without causing extra data structures to be created and manipulated at runtime. It will be easier for tools to understand what's going on, which could help in tree shaking, type systems, etc.

An attempt by LinkedIn to use the previous Stage 2 decorators proposal found that it led to a significant performance overhead. Members of the Polymer and TypeScript team also noticed a significant increase in generated code size with these decorators.

By contrast, this decorator proposal should be compiled out into simply making function calls in particular places, and replacing one class element with another class element. We're working on proving out this benefit by implementing the proposal in Babel, so an informed comparison can be made before proposing for Stage 3.

Another case of static analyzability being useful for tooling was named exports from ES modules. The fixed nature of named imports and exports helps tree shaking, importing and exporting of types, and here, as the basis for the predictable nature of composed decorators. Even though the ecosystem remains in transition from exporting entirely dynamic objects, ES modules have taken root in tooling and found to be useful because, not despite, their more static nature.

How does static analyzability help native JS engines?

Although a JIT can optimize away just about anything, it can only do so after a program "warms up". That is, when a typical JavaScript engine starts up, it's not using the JIT--instead, it compiles the JavaScript to bytecode and executes that directly. Later, if code is run lots of times, the JIT will kick in and optimize the program.

Studies of the execution traces of popular web applications show that a large proportion of the time starting up the page is often in parsing and execution through bytecode, typically with a smaller percentage running JIT-optimized code. This means that, if we want the web to be fast, we can't rely on fancy JIT optimizations.

Decorators, especially the previous Stage 2 proposal, added various sources of overhead, both for executing the class definition and for using the class, that would make startup slower if they weren't optimized out by a JIT. By contrast, composed decorators always boil down in a fixed way to built-in decorators, which can be handled directly by bytecode generation.

What happened to coalescing getter/setter pairs?

This decorators proposal is based on a common model where each decorator affects just one syntactic element--either a field, or a method, or a getter, or setter, or a class. It is immediately visible what is being decorated.

The previous "Stage 2" decorators proposal had a step of "coalescing" getter/setter pairs, which ended up being somewhat similar to how the legacy decorators operated on property descriptors. However, this coalescing was very complicated, both in the specification and implementations, due to the dynamism of computed property names for accessors. Coalescing was a big source of overhead (e.g., in terms of code size) in polyfill implementations of "Stage 2" decorators.

It is unclear which use cases benefit from getter/setter coalescing. Removing getter/setter coalescing has been a big simplification of the specification, and we expect it to simplify implementations as well.

If you have further thoughts here, please participate in the discussion on the issue tracker: #256.

Why is decorators taking so long?

We are truly sorry about the delay here. We understand that this causes real problems in the JavaScript ecosystem, and are working towards a solution as fast as we can.

It took us a long time for everyone to get on the same page about the requirements spanning frameworks, tooling and native implementations. Only after pushing in various concrete directions did we get a full understanding of the requirements which this proposal aims to meet.

We are working to develop better communication within TC39 and with the broader JavaScript community so that this sort of problem can be corrected sooner in the future.

proposal-decorators's People

Contributors

environmentset avatar freund17 avatar hax avatar jridgewell avatar kriyszig avatar kt3k avatar larrikinventures avatar littledan avatar ljharb avatar lodin avatar mbrowne avatar nicolo-ribaudo avatar pabloalmunia avatar pzuraq avatar rbuckton avatar robbiespeed avatar rwoverdijk avatar scottrudiger avatar senocular avatar techquery avatar tinychief-zz avatar tjcrowder avatar tjwds avatar tomduncalf avatar trotyl avatar valipopescu avatar vigneshshanmugam avatar xiawenqi avatar yairrand avatar yuchenshi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-decorators's Issues

Document element finishers in metaprogramming guide

After reading the proposal more carefully, I realized that there are element finishers in addition to class finishers. Assuming that this feature is likely to stay in the proposal (which I hope it is), I propose adding an example to METAPROGRAMMING.MD.

I can submit a PR for this, I just wanted to post this issue first to verify that I understand correctly and that finisher is indeed one of the valid properties you can set on the element descriptor object that you return. I was thinking I'd use the @readonly decorator I mentioned in #55 as the example:

class User {
  @readonly id
  ...

  constructor(id = null) {
    this.id = id
  }
}

function readonly(elementDecriptor) {
    elementDecriptor.finisher = () => {
        elementDecriptor.descriptor.writable = false
    }
    return elementDecriptor
}

For now I can keep my PR limited to METAPROGRAMMING.MD; I assume it's ok to include an additional example there that isn't necessarily in the readme file.

I was also wondering about this section of the metaprogramming guide:

{
  kind: "method" or "field"
  key: String, Symbol or Private Name,
  placement: "static", "prototype" or "own",
  descriptor: Property Descriptor (argument to Object.defineProperty),
}

Would it be accurate to include initializer and finisher in that list as well? Or are those only properties that you can return, not properties that are necessarily already included in the object that gets passed in? I wasn't sure if maybe the object already includes those properties but they're just undefined by default. (And in the case of initializers I'm guessing that if you initialize your field with something like class MyClass { x = 1 } that it would already have an initializer setting the value to 1 that your decorator could override/extend if it wanted to.)

Should element finishers run interspersed with static field initializers?

Example of why this would be expected to occur: If a finisher were to be used to implement a static field (e.g., a static private field), it may want to evaluate its initializer thunk. If this happens, you'd expect all of the static field initializers to be run in top-to-bottom order.

Currently, though, all finishers run after all class field initializers. This doesn't come from @wycats and @bterlson 's decorators spec; it is just an artifact of how I combined decorators with fields. Should they be interspersed? For example, the logic could be,

For each class element,
  Run its initializer, if it exists
  Run the class element's finishers (finishers added by inner decorators before outer ones)
Run the class finishers

PrivateName leak in decorators

The current semantics for the PrivateName Object can result in decorators that leak shared custom PrivateName values, breaking "hard privacy". For example:

// a.js
const sharedPrivateName = new PrivateName("shared");
export function dec(desc) {
  desc.elements.push({ 
    kind: "field", 
    placement: "own", 
    key: sharedPrivateName 
  });
  desc.elements.push({
    kind: "method",
    key: "setPrivateData",
    placement: "prototype",
    descriptor: {
      value: function(data) {
        sharedPrivateName.set(this, data);
      }
    }
  });
  return desc;
}

// b.js
import { dec } from "./a.js";

// apply the decorator, adds `sharedPrivateName` to `C`
@dec
class C {
}

// leak the private name
const desc = { kind: "class", elements: [] };
dec(desc);
const leakedPrivateName = desc.elements[0].key;

// demonstrate leak
const obj = new C();
obj.setPrivateData("private data"); // method attached by decorator

const privateData = leakedPrivateName.get(obj);
privateData; // "private data";

One possible approach to avoid this is to break PrivateName into two parts: a property "mutator" and an opaque "key". For example:

interface PrivateName {
  key;
  get(obj);
  set(obj, value);
}

Then the decorator above could be rewritten:

// a.js
const sharedPrivateName = new PrivateName("shared");
export function dec(desc) {
  desc.elements.push({ 
    kind: "field", 
    placement: "own", 
    key: sharedPrivateName.key // only send opaque key
  });
  desc.elements.push({
    kind: "method",
    key: "setPrivateData",
    placement: "prototype",
    descriptor: {
      value: function(data) {
        sharedPrivateName.set(this, data); // uses mutator
      }
    }
  });
  return desc;
}

While this still results in a key that could be added to any object, it prevents an untrusted third party from reading or writing to that state.

METAPROGRAMMING.md editorial comments

  1. Above the example for defineElement et al, you have the sentence:

    For example, the above three decorators could be defined as follows:

    I think you mean for this to refer back to the examples in README.md, as they are not defined in this file.

  2. The first line of the following example has:

    import "decorators" as decorators

    I think you mean this:

    import * as decorators from "decorators"
  3. If PrivateName is only accessible from the "decorators" built-in module, then how can scripts leverage this feature?

  4. In your example for bound, you have:

    if (placement == "prototype") placement = "instance";

    However, in the preceding definition of a class element, you have:

    placement: "static", "prototype", or "own",
  5. You have the same issue as (4) in the definition of observed:

    assert(placement == "instance")

Should extras be interspersed with elements or added at the end?

The current wording for DecorateClass puts "extras" elements at the end, after all the syntactically provided elements, in the order of the elements. This is visible in two ways:

  • If there are multiple field declarations involved, the initializer for one will see whether the other has been added.
  • In the ordering of the property descriptors, if you use some obscure metaprogramming APIs.

However, the spec by @wycats and @bterlson puts extras right after the element that it was created for (see steps 23-24).

Given that this is observable, we should get it right. I'm thinking of reverting to the previous semantics.

[Feature request] Don't introduce new tokens

The decorators proposal seems like a useful concept, but I don't see the reason for introducing new tokens like # and @.

In my opinion, it would be much better to use some old tokens for these class properties (for example / or ^ or anything which would throw a syntax error if decorators are not implemented). It will make it much easier to maintain the code readable. Why would we introduce new tokens, while we have a lot of old tokens that are currently forbidden in such context.

Merge with tc39/proposal-decorators

This specification is intended to be an update on the decorators proposal. I've been working on the champions of that proposal on it, and it seemed to be well-received at the last TC39 meeting. However, it is in a different location, which leads to confusion from people who are prototyping decorators. Maybe these contents should be moved there, e.g., with a PR.

cc @wycats @bterlson

Decorator standard library

At the January 2018 TC39 meeting, during the decorators presentation, the committee discussed a possible decorators standard library.

Would it make sense to sketch out, at an explainer level, what such a library might contain? Or, are we entirely content with leaving this to ecosystem-level solutions (e.g., core-decorators)?

At the time, some people in committee seemed to be advocating for a decorators standard library to be part of the this proposal, while others were suggesting that it would make more sense to wait until decorators are shipped for a while and see what JavaScript programmers find useful for themselves.

A compromise might be to aim to develop a Stage 1 proposal for a decorator standard library while this decorators proposal is at Stage 2 or 3. Even if it stays at Stage 1 for a while, such a standard library might be a useful design for polyfills and gain usage organically that way.

You may enjoy this paper on C++'s proposed "metaclasses"

https://herbsutter.files.wordpress.com/2017/07/p0707r1.pdf

It seems that this approach has a lot in common with the current "class decorators" approach, from my brief skimming of the updated markdown documents in this repository. It might be worth doing a careful study to see if there are interesting elements from that paper that we could learn from, or borrow, or that we aren't covering with class decorators.

Obviously the setting is very different (compile-time vs. runtime). But I think the effect is similar. And I was struck by the examples that paper presents (new "types" of classes, e.g. interface, "ordered type", "value type", etc.) which are very different from the @defineElement() example in this repository. Indeed, that paper makes me wonder if class decorators are general enough to replace other possible proposals (e.g. the traits-like protocol/interface proposal).

Bugfix: isStatic

First of all, thank you for putting the proposal together.

Small bug to be fixed:

  • This is how descriptors are presented in METAPROGRAMMING.md:
    {
      kind: "method" or "field"
      key: String, Symbol or Private Name,
      placement: "static", "prototype" or "own",
      descriptor: Property Descriptor (argument to Object.defineProperty),
    }
  • Later on placement is not used. Instead of it isStatic is used in several places:
    memberDescriptor.extras = [{
      kind: "field",
      // The initializer callback is called with the receiver set to the new instance
      initializer() { return descriptor.value.bind(this); },
      descriptor,
      isStatic,
    }];

The intend is clear to me, but probably it should be fixed anyway.

Should we allow non-writable private fields? writable private methods?

The current decorators proposal gives decorators the ability to manipulate the writability of private fields and methods. Enumerability and configurability don't quite make sense for private fields (it can't be enumerable if it's private, and delete is a syntax error), but writability works.

From advice from @bakkot, this proposal does allow decorators to manipulate the writability of private fields and methods, since there was no reason not to. But at the same time, I don't have much of a use case for this manipulation in mind. Given that there are a number of lines of spec text prohibiting property descriptors in certain states, it wouldn't be so out of the ordinary to prohibit this.

I don't have a strong reason to go either way. Any opinions?

Improve branding on descriptors

Right now, the only way to tell if a class descriptor, method descriptor or field descriptor is that or another object is to read its properties and see if it kinda looks like a descriptor (e.g., does it have a type property). To make overloading easier and more reliable, we should brand them. We can do this through an own @@toStringTag property that identifies these as "Class Descriptor", "Method Descriptor" and "Field Descriptor". This should resolve tc39/proposal-decorators-previous#24 .

Decorate a class but not an instance

I need to define some static methods or properties in the decorator. But in the current version there is a reference only to the instance but not to the class.


myDecrator(...args) {
   needMyClass.staticProperty = 'SomeValue';
   ^^^^^^^^^^^^ need reference to decorated class in this stage
  return myConsructor(instance) {
     instance.property = 'someValue'l
  }
}

Consider `protected`, `abstract`, and `friend`

While this is likely better suited for a separate proposal, I wanted to outline my early thoughts about accessibility modifiers, as a follow up to #24 (comment). If this seems generally viable, I may work this up into a full proposal.

The protected modifier

The protected modifier allows a subclass to access all of the private names of a superclass that were declared as part of a protected member.

Example

class Super {
  protected #x = 1;
  protected #y() { return this.#x; }
  constructor(x) { this.#x = x; }
}

class Sub extends Super {
  protected #y() {
    // overrides #y in Super
    return super.#y() + 1; // ok, since its the same #y
  }
  method() { return this.#y(); }
}

Suggested Semantics

  • To implement protected in a fashion that aligns with other languages, private named methods would need to be installed on the prototype and member lookups would require a prototype walk, as this is needed to support super.
  • Whenever a class extends a superclass, the private names of any protected method are added to the subclass instance.
  • A subclass may "override" a protected member of a superclass.
  • A subclass may not declare a new private member with the same name as a protected member of its superclass.

The abstract modifier

An abstract modifier could be applied to classes and both private and non-private methods.

Example

abstract class Super {
  protected abstract #x(a, b); // protected abstract
  abstract y(c); // public abstract
}

class Sub extends Super {
  protected #x(a, b) {   }
  y(c) { }
}

Suggested semantics

  • An abstract class has a specialized [[Construct]] method that throws if the current new.target is a constructor for a class that is abstract.
  • An abstract method must be declared in an abstract class.
  • An abstract method may have a parameter list, but its parameters may not have initializers and may be neither binding patterns nor a rest argument.
  • An abstract method may not have a body. When the method is defined during class declaration evaluation, it is given a body that throws a TypeError.
  • An abstract method with a private name must also be declared protected.
  • If a subclass is not abstract, but it directly extends an abstract superclass, it must provide an implementation for any abstract method.

The friend modifier

The friend modifier allows a class to indicate another class that can access its private and protected members. The befriended class can access those private names using a prefixed private name.

Example

class B {
  getX(obj) {
    return obj.A#x;
  }
  setX(obj, value) {
    obj.A#x = value;
  }
  callY(obj) {
    return obj.A#y();
  }
}

class A friend B {
  #x;
  protected #y() { return this.#x + 1; }
  constructor(x) { this.#x = x; }
}

Suggested semantics

  • When declaring the friends of a class, the befriended class must already be declared and initialized.
  • The prefixed-private name has the form: Identifier # Identifier. The lexical this of the caller is passed as a new argument to the private get operation and is used to check access.

Optional: The private modifier

The private modifier is only allowed on a private-named declaration and is optional. Generally it just exists to visually disambiguate between private and protected members.

Should PrivateName be a frozen/defensible class?

In a meeting discussing decorators with potential implementers, @gsathya, @msaboff and @akroshg expressed some concern about the implementation complexity of adding a new PrivateName primitive. The current specification requires handling PrivateName in all sorts of contexts--this could be complexity which grows over time as the language evolves.

Is it possible to avoid exposing PrivateName directly, and instead use some kind of closures, but still get at the same kind of expressivity? Let's brainstorm API ideas.

Decorator for onReject function for async methods

These decorators may be useful for interfacing legacy code or browser event handlers in a way that is easier to read.

With ECMAScript 2017, much code is classes of async methods that is frequently called by code that is not class-oriented.

A case often discussed is bind to this, which today is used in the constructor with this.method = ::this.method

Another frequent case is for an event invocation to transition into an async method. Here, it would be of benefit if both this binding and an onReject function could be defined

today, ECMAScript 2017:

class {
  handler = e => (async () {
    โ€ฆ
  })().catch(this.errorHandler)

  errorHandler = e => console.error(this.e = e)
}

if that could instead be somehow like:

class {
  @catch(this.errorHandler)
  async handler::(e) {
    โ€ฆ
  }

  errorHandler::(e) {
    console.error(this.e = e)
  }

With the :: meaning this.method = this.method.bind(this)
Then those methods would not be anonymous and there would be less ugly characters to get wrong

Other ways of declaring private fields

I'm not sure if this is the best place to ask. Since I couldn't get a clear answer why other methods for declaring private fields where not considered in this thread tc39/proposal-class-fields#59
I would like to know if here somebody could clarify.

To sum up,
I read the FAQs and I get why the # chars was chosen. What I disagree with is the way private fields are declared.
In the discussion I mentioned the following example to be confusing:

class {
  $validField = 1
  _validField = 2
  #validField = 3 // This is private, but looks similar to the above
  method() {
    this.$validField
    this.#validField
  }

Here, there is nothing that makes the code self explanatory. $ and _ are still perfectly valid chars to define a variable.
What about:

class {
  $ = jQuery
  _ = lodash
  #$ = Other
}

I asked later in the thread, why isn't this syntax better?:

class {
  $validField = 0 // public
  _validField = 0 // public
  private #validField = 0 // code self explains it's a private var
  private #$validField = 0 // less confusing
  static private #_validField = 0

  method() {} // public
  $method() {} // public
  private #method() {} // private
  private #$method() {} // private
  static private #method() {} // private

  constructor () {
    this.#method() // they way of accessing doesn't change as proposed before
  }
}

Why?
Public fields are defined without the public keyword
Static fields are defined with the static keyword
Private fields are defined the same way public fields are with special chars _ and $ which makes it confusing and the code less self explanatory
By not using a keyword private, it may limit future proposals for other kinds of fields (protected, friend for instance. Would they need to come up with another unused character?)

Could somebody clarify if these points where taken into account? If so, does that mean defining vars beginning with _ and $ should be considered a bad practice in the future to avoid making the code not readable when working with private fields?

@bound example - documentation suggestion

It has become a common practice (especially in the React community) to create bound methods using a property initialized to an arrow function:

class MyComponent extends Component {
    handleClick = (event) => {
        ...
    }
    ...
}

So at first I was surprised to see that the readme for this proposal has a @bound decorator as one of the examples, given that using an arrow function seems more straightforward. Then I found this:
tc39/proposal-class-fields#80

...and I remembered that I myself had previously encountered issues with inheritance when using arrow functions in this way.

I'm guessing that this is why a @bound decorator is used as an example - since it's a more consistently readable way to bind methods, and thus preferable to an arrow function.

In any case, it could be helpful to mention something about this in the readme, in case others also wonder, "Why not just use an arrow function"? I understand if this is a low-priority change, but thought it was worth suggesting.

On `extras`

extras semantics should be better defined. Right now it is possible to define the same property in extras that belong to different original properties. It is even possible to define the same property more than once in the same extras array. What is the protocol here?

While it may indicate a bug, if somebody did it inadvertently, I suspect that the use case for that can be third-party libraries, which intentionally decided to create a property with the same name. Examples:

  • A decorator wants to provide a default implementation for a certain method, if it is not defined by a user or some other library. Or piggy-back on it.
  • I want to override destroy() method in different decorators, so I can destroy/finalize objects created to fulfill decorators purpose (delete HTML elements, remove event listeners, flush streams, notify a server that I am done, and so on).

Ideally it would be nice to have a way to reconcile such duplicates with a callback, and provide a reasonable default for it. finisher() looks like the right place for that. For that it should receive dups as a parameter, so potentially it can make sense of it. The default action can be as simple as overwriting duplicate properties indiscriminately.

Just for completeness: if it is possible to add more properties, why it is not possible to remove some of them? The simplest example would be a meta-property, which is saved to guide other decorators, or reflection methods, yet should not be preserved on a property/instance/constructor.

Add "rebind decorators"?

When you see a declaration of class C { @decorator #x }, there is no way in the current proposal for @decorator to make #x to refer to a different private name. Sure, the decorator can output a different private name in the key property, but that doesn't actually rebind what #x points to.

If we could rebind the keys, then we could make more ergonomic friend decorators, and possibly more efficient protected decorators. Friend decorators could look like this:

let key = new FriendKey();

class FriendClass {
  @expose(key) #x;
}

class Accessing {
  @use(key) #x;
  method(friendInstance) { friendInstance.#x }
}

Without the ability to rebind, usage patterns would look more like explicitly calling a function to read the private field.

Rebind could be another kind of decorator. It would just rebind a syntactic private name like "#x" to a different Private Name primitive. If a decorator wants to also create a method or field with that name, it can do so with extras.


An alternative would be to allow decorators to return a different element descriptor with the private name changed to a different private name. The decorator specification could see that a different private name was used, and interpret this as an instruction to also rebind the syntactic private name.

If we go with this option, we may want to make decorators, in general, prohibit changing the key. That way, when the key does change, it's interpreted as rebinding.

The funny thing about this option is, it would end up giving a private field or method to the class which is accessing the friend field or method. I can't think of a case where we'd want this behavior.


Another alternative would be to provide a different binding construct within classes to rebind private names. For example, this could be

class Accessing {
  private #x = FriendKey.use("#x");
}

Such a binding construct could be available outside of classes as well, potentially.

move (at least) open issues from previous repository

there are still open issues in the previous repository that are still relevant, but the aren't particularly visibly from here. They should be "out of sight, out of mind"

I just spent 10 minutes trying to figure out what was going one when a followed a "see it on github" link for such an issue and it brought me here to a completely different issue.

Scala-like multiple-parameter lists for decorator functions with arguments

I wonder if it would be worthwhile to pursue a syntax extension for functions to allow for multiple-parameter lists:

function defineElement(tagName)({ kind, elements }) {
    assert(kind == "class");
    return {
      kind,
      elements,
      // This callback is called once the class is otherwise fully defined
      finisher(klass) {
        window.customElements.define(tagName, klass);
      }
    }
  }
}

Probably not now, but perhaps worth considering at a later date if there's enough of a use case.

Early error for misuse of decorators on getter/setter pairs?

We've discussed in committee the idea that coalescing of class elements exists so you use one decorator that applies to a getter/setter pair. Coalescing takes place at runtime, since computed property names may be in use, so in general we don't have this error until the class definition actually executes. However, sometimes it will be apparent already at static semantics time--we could create an early error when non-computed names or private names form a getter/setter pair where each one is separately decorated. Should we do that?

Use case for exposing field initializer functions

Does anyone have an idea for a use case for manipulating the initializer for a field? I can't think of one. It seems like you usually want to leave it in place. Maybe you want to create an initializer for your own synthetic private field, but do you need to see the initializer for an existing one, e.g., wrap it in something?

I guess one use case is, in the @observed/@tracked decorator, you want to take the initializer and put it on the underlying private field, replacing the previous field with an accessor pair.

Thanks to @adamk for raising the question.

Decorator call expressions

The grammar for decorators allows the following expressions:

@A
@a.b.c.d
@a.b(c)

but not:

@A(b).c
@A(b)(c)

This inconsistency with call and dot expressions seems odd.

Also, proposal does not define what it means to evaluate those decorator expressions, although that's pretty obvious by analogy to existing expressions.

Document friends and protected better

This proposal can currently support a few different frequently requested features for private fields and methods.

For protected fields or methods, it's possible to create decorators such as the following (as @bakkot demonstrated).

class Superclass {
  @protected #x;
  @protected #y() { }
}
class Subclass extends Superclass {
  @inherit #x;
  @inherit #y;
  method() { use(#x); use(#y()); }
}

For friends, it's possible to expose these things into a designated object, though accessing them would be a bit more awkward. It looks more like a metaprogramming construct.

let friendKey = new FriendKey();

class FriendClass {
  @expose(friendKey) #x;
  @expose(friendKey) #y() { }
}

class OtherClass {
  method(friendInstance) {
    use(friendKey.get(friendInstance, "#x"));
    friendKey.set(friendInstance, "#x", value);
    friendKey.call(friendInstance, "#x", arg);
  }
}

These are sort of documented in METAPROGRAMMING.md but that document is a bit difficult to read, and not opinionated or focused enough. Document these options better.

Allow plain thunks to execute as a decorator element

Proposal: Allow undefined as a key for field decorators. The semantics would be to just execute the initializers for its side effect. (Another superficial alternative would be to make this a new element type, which doesn't have a key field.)

A couple reasons this comes up:

  • For static: If lexical declarations are added to classes, I'd imagine they would execute code in a manner which is interspersed with static field declarations. A class decorator can see and manipulate static field declarations. Therefore, we need some reification of the execution of lexical declarations, if only to preserve the way that they are interspersed. In this proposal, each lexical declaration's initialization would be thunked and put into the class element list. (Note: this means that we'd want to make arguments an earlier error in lexical declarations.)
  • For instances: Often a decorator will want to run code as part of instance construction, towards the beginning of construction. This would provide that. Counter-point: Some of the time, the code that you want to run is actually at the end of construction, and an "instance finisher" would be more appropriate. (I've come across cases where the "beginning" semantics seemed more appropriate, but I can't remember them at the moment.)

Should PrivateName be a kind of Symbol?

PrivateNames are presented as a new primitive type. They are significantly different from Symbols in that they are not property keys, e.g., they don't work in [] and don't go through Proxy traps. However, in other ways they are somewhat analogous, in that it's a value which is used to access something related to an object.

Would it make sense to make being 'private' a 'bit' on Symbol? Instead of making a brand new primitive type, it could be a kind of symbol which is not a property key.

Ultimately, this is a distinction about aesthetics; I don't think it will result in a significant change in implementation or semantics.

Should decorators have access to private state?

First of all, thanks for putting together this proposal!

This feedback is based on the behavior that I think the proposal has from reading over the markdown files in this repo. I'm not sure I'm fully understanding the behavior, so some of this feedback might be based on a misunderstanding of how things work -- please feel free to correct me if that's the case.


Based on this paragraph, it seems like decorators have access to create and read private names on a class. The justification for this, from what I can tell, is that it would be convenient to use decorators to avoid duplicating common operators for private fields (e.g. by deprecating a private field with @deprecated, or by using an @observed getter to update a model).

I think there are compelling reasons to disallow decorators from accessing private state:

  1. It breaks the encapsulation provided by private state. In my opinion, the biggest advantage of private state is that it's very easy to reason about fields locally, and fields can be renamed or refactored out within a single class without fear of breaking anything. It should always be safe to use find/replace within a class to rename a private field, but if an external decorator function can perform operations on private fields, this will no longer be the case.
  2. The proposed use cases for private field decorators are unconvincing to me.
    • It shouldn't be necessary to deprecate a private field with a @deprecated decorator -- since the field is private, one could safely remove it and replace all instances with something else. A developer could also find all usages of the field just by searching for #foo in the class.
    • Along similar lines, I'm not seeing the advantage of using an @observed getter on a field rather than a private method. Using getters with side-effects is typically considered an anti-pattern anyway.
  3. It would be useful to identify invalid private field references with static analysis. This won't be possible if decorators can create private fields at runtime. (edit: this would still be possible, see #4 (comment))

Stage 3 reviews

Thanks to everyone who signed up to do a Stage 3 review of decorators for the March TC39 meeting. Filing bugs in this repository is a great way to give feedback.

If it would be useful for any of you, I'd be happy to schedule a call where we can go over the proposal as a small group and we can discuss issues that way. Just let me know.

If you have reviewed this proposal and want to sign off on it as is, please say so on this thread and I'll check the box.

Editors:

Proposal: Splitting PrivateName into its own Proposal

tldr; I want to split PrivateName into another proposal so its API can be worked on.


With the current proposal (at least as of the Sep. meeting), PrivateName has become has become extremely intertwined with decorators.

  • The constructor only serves a purpose as a field decorator's key
  • The get and set functionality is only exposed as parameters to the decorator call

I think this really hinders the potential meta-programming that could be done with a "secure weak map". Specifically, I'd really like to see an API that totally independent of decorators, and potentially for any Object (not just classes).


The API I imagine is a specialized WeakMap:

// I'm writing this as JS. Imagine it's really an API contract
class PrivateName extends WeakMap {
  constructor() {
    // PrivateNames are not directly constructible, requires special syntax to create
    throw new Error('PrivateNames are insecure when instantiated without syntax');
  }

  get(key) {
    if (!this.has(key)) {
      throw new Error(`PrivateName ${this.description} is not initialized on key`);
    }
    return super.get(key);
  }

  set(key, value) {
    if (!this.has(key)) {
      throw new Error(`PrivateName ${this.description} is not initialized on key`);
    }
    super.set(key, value);
  }

  // #add is absolutely necessary if we decouple PrivateNames from decorators.
  add(key, value) {
    // Note, this throws if slot has already be added to the key.
    if (this.has(key)) {
      throw new Error(`PrivateName ${this.description} already initialized on key`);
    }
    super.set(key, value);
  }

  // #has is specifically for meta programming. Much easier than wrapping a #get call in a try-catch,
  // which will be what everyone will be forced to do without it.
  has(key) {
    return super.has(key);
  }

  // I'm iffy on #delete. But if we have an #add, a #delete is probably the necessary inverse.
  delete(key) {
    super.delete(key);
  }
}

Ignore prototypes and constructors for the moment, I'll address them with secure-ness a bit later.

As I mention in the comment, #add is absolutely necessary if we want to decouple PrivateNames and decorators. If this is really just a specialized WeakMap, we need a way to add the object into the PrivateName. Decorators can later directly use this method when initializing the slot during class instance construction.

Using #add
class Test {
  constructor(value) {
    privateX.add(this, value);
  }
}
new Test();

// I work with Objects, too.
const obj = {};
privateX.add(obj, 1);

Decorators may internally use #add during construction:

function dec(desc) {
  desc.elements.push({ 
    kind: "field", 
    placement: "own", 
    key: privateX 
  });
  return desc;
}

@dec class Test {
  // Internally, the decorated field is using #add
  // constructor() {
  //   privateX.add(this);
  // }
}

From @littledan's comments to me, this might be a pain point for implementors, since it would basically be modifying the internal structure of the object. I'm hopeful that this can be optimized away when using decorators, so that they're like pure internal slots when declared in the decorator's elements. As for manual adding, I think this is a necessary cost so that we can have truly meta-programable private state.

As for #has and #delete, these are to round out the API. I can already imagine that if we don't have a #has, I'm going to wrap #get in a try-catch to get the same functionality. And #delete seems like the inverse of #add. I just don't have a specific use case for it yet.


Now, how do we address secure-ness, while still providing a decent object-oriented API? Specifically, how can we guarantee that PrivateName constructor is the real native, not some monkey patched class, and how do we ensure that #get, #set, etc aren't monkey patched either?

I think we can get around this by making the PrivateName property on the global object non-configurable/writable. If that's not acceptable, maybe disallow explicit construction in favor of an explicit syntax?

private privateX; // Invokes constructor
privateX.add(obj, value);

// While this throws
new PrivateName('test'); // cannot invoke directly

Then, #get, #set, etc are not prototype methods, but own-properties that are set to %PrivateGet%, %PrivateSet% internal functions.

I think this avoids issues with any monkey patching.

Allow decorators to manipulate the private name scope?

Should decorators be able to create, rename and remove private names which are visible to class bodies? For example, we could have a @guid class decorator, which creates a #guid private field, which is accessible within the class body. Or, an @inheritProtected decorator could let you see the protected fields and methods of a parent class, accessible with # syntax directly, even without any statements in the class body that bind the name.

So far, I've shied away from this possibility, and tried to keep the private name scope "static". For example, programmers can depend on the fact that, when you see a syntactic #x in a method or initializer body, you can tell which enclosing class defined it by just looking for all the private field and method declarations. A decorator may rebind the value to something else, but you won't get a #x out of nowhere that doesn't correspond to a declaration. More flexibility feels a bit like with to me.

However, this flexibility may be useful for programmers. @bterlson raised this issue with me; I'd be interested in more input here.

typo in bound decorator sample code?

In METAPROGRAMMING.md

// Replace a method with a field with a bound version of the method
function bound(elementDescriptor) {
  let {kind, key, placement, descriptor} = elementDescriptor;
  assert(kind === "method");
  if (placement == "prototype") placement = "own";
  function initializer() { return descriptor.value.bind(this); }
  delete descriptor.value;
  return { kind: "field", key, placement, descriptor, initializer };
}

It seems these two lines

  function initializer() { return descriptor.value.bind(this); }
  delete descriptor.value;

are not correct which cause descriptor.value be undefined when initializer is called.

I guess it should be:

  const {value} = descriptor;
  function initializer() { return value.bind(this); }
  delete descriptor.value;

Preserve Hard Privacy

The current proposal uses a globally exposed prototype to explain PrivateName. This causes some leakage of private state by snooping using:

const old = PrivateName.prototype.set;
PrivateName.prototype.set = (o, v) => {
  log(o, v);
  return old.call(this, o, v);
}

This can be used a number of ways (implementing getter/setter functionality, spies, attack, etc.), but I believe invalidates the goals of private state.

This can be somewhat easily changed to not use a shared reference that JS can get to. I am open to any ways to accomplish this, but have an idea below.

  1. Change placement to include "private". Privacy is different from "own" in that others cannot access it by reflection alone.

  2. When creating a descriptor for private fields, change the type to be an AccessorDescriptor bye default, with an internal field to the private field. I think converting it to a DataDescriptor should also be fine while working on it.

descriptor = {
   #field: #foo
   configurable: true,
   get() {},
   set(v) {},
}
  1. Private field creation does more than mutate existing declarations on the class. I would like to see it be possible, but will not want it breaking hard privacy. If it is needed, for now a WeakMap does work. For now, I would leave mutating the class by adding private fields. The alternative is to return the descriptor on creation like above instead of using the global prototype approach, which I think would seem ok but odd.

Decorator use cases

According to the meeting notes here there is a slide deck illustrating decorator use cases. Unfortunately, the link to the slides is broken. Are those slides published anywhere?

Allow decorating functions

Hi,

At the bottom of the README, one can read "Arguments and function declarations cannot be decorated.".
Is there a reason why functions should not be decorated?
Coming from a Python development background, my (only) use-case for decorators is to wrap functions, allowing me to extract redundant logic, short-circuit and do other great meta-programming things.

I would expect to be able to write code like:

@myDecorator
function myFunc(args) { 
    // code 
}

and

@myDecorator
const myFunc = (args) => { 
    // code 
}

Edit Maybe a better way of writing lambdas with decorators:

const myFunc = @myDecorator (args) => { 
    // code 
}

Argument decoration seems like a great feature as well:

function myFunc(@myDecorator callback, otherArgs) { 
    // code 
}

Thank you!

How would a `@callable` class decorator work?

I noticed that there's no clear way to replace a class with a function (or something else that's not the class itself), and it'd be very useful to a lot of consumers. It'd address the if (!(this instanceof Foo)) return new Foo(...args) ES5 idiom by providing a sufficient replacement, as initially proposed in the call constructor proposal, which itself was rejected in favor of a decorator-based solution.

Re-add element finishers

This proposal removes finishers from methods and fields. However, finishers may be useful there in order to add runtime metadata to methods and fields.

Formalize the definition of coalescing

Previous discussion: tc39/proposal-decorators-previous#14

The definition of coalescing syntactic getters and setters is a bit high-level:

Let elements be elements with getters and setters of the same key coalesced, with later declarations overriding earlier ones.

This should be formalized with an actual algorithm. The algorithm should be equivalent to the old ValidateAndApplyPropertyDescriptor sequence that is used for ordinary things in classes, or a subset of its behavior (i.e., syntax errors some of the time), but not a different interpretation of the same combination of declarations.

Clarification on proposed meta information

TAXONOMY and METAPROGRAMMING define a structure to describe a decorated element:

{
  kind: "method" or "field"
  key: String, Symbol or Private Name,
  placement: "static", "prototype" or "own",
  descriptor: Property Descriptor (argument to Object.defineProperty),
}

It appears that kind is largely redundant: it can be derived from a property descriptor:

function getKind (prop) {
  return prop.get || prop.set || typeof prop.value == 'function' ? 'method' : 'field';
}

Granted that someone can define a field initialized with a function, but it is โ€ฆ a method for all intents and purposes, and probably should be treated as such.

"Class decorators are passed in an Array of all class elements". Why exactly? To check elements by names linearly? To have them unsorted by placement? It appears that if we re-use an existing structure we gain utility and simplicity. I am talking about a dictionary of property descriptors consumed directly by Object.defineProperties() and Object.create(). If a class decorator receives three objects corresponding their placement, and can modify them in place, it will simplify a lot of things. (Obviously a class decorator needs to know the base class as well.)

Example: verify that render() is defined, add a default implementation otherwise:

const ensureRender = (proto, stat, own) => {
  if (!proto.render && !own.render) {
    proto.render = {value: (() => {}), configurable: true, writable: true};
  }
};

The example above is trivial. The array version will be less readable and much slower.

Example: do-it-yourself class in a class decorator:

const doItYourself = (proto, stat, own, Base) => {
  // merge our custom code with initialization of own
  const Ctor = makeConstructor(proto.constructor, own, Base);
  // take care of the prototype
  Ctor.prototype = Object.create(Base.prototype, proto);
  // take care of static definitions
  Object.defineProperties(Ctor, stat);
  // ...
};

Again, all code above is a small collection one-liners. The only non-trivial part is to generate a constructor, which will call the base, initialize own properties, and call a user-defined custom constructing code. It should be done anyway is a part of this proposal.

The proposal mentions, but does not define, a class finisher. The code above could very well be a spec for the default finisher.

Example: go back to the original array of elements:

const getKind = prop => prop.get || prop.set ||
  typeof prop.value == 'function' ? 'method' : 'field';

const getKey = name => 
  typeof name == 'string' && name.charAt(0) === '#' ?
    decorators.PrivateName(name.substr(1)) : name;

const generateElement = (placement, props) => name => {
  const prop = props[name];
  return {placement, key: getKey(name), kind: getKind(prop), descriptor: prop};
};

// convert to the old structure in a class decorator
const converter = (proto, stat, own) => {
  const old = [
    ...Object.getOwnPropertyNames(proto).map(generateElement('prototype', proto)),
    ...Object.getOwnPropertySymbols(proto).map(generateElement('prototype', proto)),
    ...Object.getOwnPropertyNames(stat).map(generateElement('static', stat)),
    ...Object.getOwnPropertySymbols(stat).map(generateElement('static', stat)),
    ...Object.getOwnPropertyNames(own).map(generateElement('own', own)),
    ...Object.getOwnPropertySymbols(own).map(generateElement('own', own))
  ];
};

Isn't it simpler and more flexible this way than an array of elements?

Multiply used private name collisions

From @rbuckton during the September meeting:

Private name API. Decorators on two different class members that add an "extra" with the same name. Collisions? Talk offline.

I don't understand the concern. What would the collision be? You'd
just have two classes with an extra with the same name, and code that
accesses one would be able to access the other. Could you explain what
the problem is?

decorator on `let`

I get that the decorator on function is hard to implement due to the hoisting so they are out. Actually let over lambda(closure) is the same thing as the class, so it is justifiable for us to want to have a mechanism to make decorator works on function.

The let keyword don't have a variable hoisting and the variable defined by let is mutable. So we could make let to take the responsibility.

Example

define functions as decorator

let logger = x => {
    console.log(x);
    return x;
};

let inc = x => x + 1;

use case

@logger
@inc
let a = 5;
let b = "blablabla";

transform into

let a = 5;
a = inc(a);                       // a: 5 -> 6
a = logger(a);                    // logs "6"
let b = "blablabla";

It will also works on function expression defined by let when the decorator is a hyper function.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.