Decorators Update:

Removing @init

Chris Hewell Garrett

2021-12

Refresher: Decorator Capabilities

  • Replacement
  • Metadata
  • Access
  • Initialization when used with `@init:`

Refresher: @init

Users can prefix a decorator with `@init:` to add the ability to add initializers to the value

  • Initializers added via `addInitializer` on context obj
  • Class element initializers run during class construction for each instance of the class
  • Class initializers run after the class has been fully defined (e.g. after static fields have been assigned)
function bound(fn, { name, addInitializer }) {
  addInitializer(function() {
    this[name] = this.name.bind(this);
  });
}

class MyClass {
  message = 'Hello!';

  @init:bound callback() {
    console.log(this.message);
  }
}

let { callback } = new MyClass();

callback(); // Hello!

Why @init?

Design was originally motivated by making decorators more statically analyzable.

@init initializers were interleaved with class field initializers. This meant that implementations would have to check for additional code to run after each decorated element, before the next class field. Can no longer generate static bootstrapping code for class fields based on class shape.

class MyClass {
  // This field is assigned _before_ the 
  // initializers for `method` are run. This
  // means that this field uses the 
  // uninitialized version of `method.`
  uninitialized = this.method();

  @init:decorate
  method() {
    // ...
  }

  // This field is assigned _after_ the 
  // initializers for `method` are run. This
  // means that this field uses the 
  // fully initialized version of `method.`
  initialized = this.method();
}

Why remove it?

  • @init was one of the most controversial aspects of the new proposal.
  • Additionally, no use cases required the interleaving.
  • UX is confusing, it results in users being able to see/interact with uninitialized versions of methods and other.
  • Puts the onus on users of decorators rather than authors to know when to add `@init`
class MyClass {
  // This field is assigned _before_ the 
  // initializers for `method` are run. This
  // means that this field uses the 
  // uninitialized version of `method.`
  uninitialized = this.method();

  @init:decorate
  method() {
    // ...
  }

  // This field is assigned _after_ the 
  // initializers for `method` are run. This
  // means that this field uses the 
  // fully initialized version of `method.`
  initialized = this.method();
}

Updated proposal

  • Initialization is now considered a core capability of decorators.
  • Initialization provided in 1 of 2 ways
    1. For elements that are per-instance, decorators can replace the initializer.
    2. For elements that are per-class, decorators can add initializers with `addInitializer`
  • Initializers run all at once before class fields are assigned 
  • Static decorators and class decorators run initialization once during class definition.
class MyClass {
  foo = this.method();

  @decorate
  method() {}

  bar = this.method();
}

// Loosely equivalent transpilation:
class MyClass {
  static {
    this.method = decorate(this.method, { /*...*/ })
  }
  
  foo = (runInitializers(), this.method());

  method() {}

  bar = this.method();
}

Other consequences

class MyClass {
  // This was possible before, no longer possible
  @init:nonenumerable foo = 123;

  // This was never possible. This would run once
  // per instance, meaning it would be a really
  // inefficient way to change the enumerability.
  @init:nonenumerable
  method() {
    // ...
  }
}
  • No way to redefine a class field. This means you cannot change enumerability, configurability, etc.
    • This capability was never provided consistently to all decoratable values with this proposal, so this is a more consistent design overall.

Questions?

@Thanks!