Decorators Update

Chris Hewell Garrett

2021-07

1  Proposal Overview

2  Current Status

  • Decorators are plain JavaScript functions
  • Can be applied to
    • Classes
    • Class elements
  • Three main capabilities
    • Replacement
    • Metadata
    • Access
  • ​Additional capabilities via modifiers (e.g. `@init:` syntax)
  • Also includes a new class element: auto-accessors

Proposal Overview

function decorate() {
  // ...
}

@decorate
class Example {
  @decorate myField = 123;

  @decorate accessor myOtherField = 456;
  
  @decorate myMethod() {
    // ...
  }
  
  @decorate get myAccessor() {
    // ...
  }

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

Decorator Function API

function decorate(
  value: Input,
  context: {
    kind: string;
    name: string | symbol;
    access?: {
      get?(): unknown;
      set?(value: unknown): void;
    };
    isPrivate?: boolean;
    isStatic?: boolean;
    addInitializer?(initializer: () => void): void;
    getMetadata(key: symbol);
    setMetadata(key: symbol, value: unknown);
  }
): Output | void;
  • First argument is value being decorated
  • Second argument is context object providing information about type of value + additional APIs
  • Return value is the replacement value
  • Input and Output type depend on value that is being decorated

Decorator Capabilities: Replacement

Decorators may replace the value that they are syntactically applied to with another value of the same type

 

 

 

 

 

They cannot:

 

  • Class decorators return a new class, method decorators a new method, etc.
  • Field decorators return pipelined initializers (do not receive initializer in decorator itself)
  • Replace with a different type of value
  • Add, remove, or replace values other than the decorated value
function logged(method) {
  return (...args) => {
    console.log(`${method.name} called`);
    return method(...args);
  }
}

class MyClass {
  // myMethod is replaced with the 
  // logged version
  @logged
  myMethod() {
    // ...
  }
}

// Will throw an error because the 
// decorator does not return a constructor
@logged
class MyOtherClass {}

Decorator Capabilities: Metadata

Decorators may add metadata to an element which can then be read later and used by other readers/libraries

 

const VALIDATIONS = Symbol();

function isString(method, context) {
  context.addMetadata(
    VALIDATIONS, 
    (v) => typeof v === 'string'
  );
}

function validate(obj) {
  let validations = obj[Symbol.metadata][VALIDATIONS];
  
  for (let key in validations.public) {
    let validator = validations.public[key];
    
    if (!validator(obj[key])) {
      return false;
    }
  }
  
  return true;
}

class MyClass {
  @isString someString = 'foo';
}
  • Metadata is accessed via `Symbol.metadata`
  • Metadata must be namespaced under a symbol, to help prevent collisions
  • Metadata is inherited

Decorator Capabilities: Access

Decorators may provide access to an element to collaborative code that works with the decorator, usually via metadata

 

  • Decorators on public values provide access via the string name of the value
  • Decorators on private values provide access via a `get` and `set` function which can be used to access the value
const EXPOSE = Symbol();

function expose(val, context) {
  let { name, access } = context; 

  context.setMetadata(EXPOSE, { name, access });
}

class MyClass {
  @expose #value = 123;
}

test('Private field is a number', (assert) => {
  let { private } = MyClass[Symbol.metadata][EXPOSE];

  let { get } = private.find((m) => m.name === '#value');

  let instance = new MyClass();

  assert.equal(typeof get.call(instance), 'number');
})

Decorator Modifiers: @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!

New Class Element: Auto-Accessor

Auto-accessors are similar to class fields, but have a getter and setter defined instead

  • Can be used to make a "field" on the prototype
  • Can also be decorated directly, allowing decorators to provide reactivity and intercept access
  • Can possibly be extended in the future with a more general syntax (see proposal here)
class MyClass {
  accessor someValue = 123;
}

// Similar to
class MyClass {
  #someValue = 123;
  
  get someValue() {
    return this.#someValue;
  }

  set someValue(value) {
    this.#someValue = value;
  }
}

// Can be used to intercept gets/sets
class MyClass {
  @reactive accessor someValue = 123;
}
  • Decorators champion group has internal consensus with the details of the current proposal
  • Spec has been written, is available at:
  • Experimental transpiler has been implemented:
  • Babel plugin in the process of being updated
    • Babel plugin will add additional community feedback from a larger audience
    • Once the Babel plugin is released and has had some time for testing, will propose for stage 3  
  • Seeking reviewers for Stage 3

Current Status

Questions?

@Thanks!