Photo by Annie Spratt on Unsplash

The Ecma TC39 committee, which standardizes the JavaScript language (officially known as ECMAScript), has been discussing a decorators proposal for several years. Transpilers like TypeScript and Babel implemented the initial version of the decorators proposal, allowing developers and frameworks to start using the proposal before the feature became an official part of the language standard. However, the proposal has seen significant changes through the standardization process. Decorators will soon arrive in JavaScript, and there are many questions to answer.

What are decorators and why should you care?

Decorators are a metaprogramming language feature, which allows modifying declarations like classes. This pattern looks at program source code as data and allows analyzing and transforming of code. For example, decorators may add add logging to the inputs or outputs of methods. Alternatively, a decorator could add some metadata to a class to configure a framework.

@addMetadata('name-for-library')
class MyClass {
  constructor(database) {
    This.db = database;
  }

  @logOutput
  makeDatabaseCall(param) {
    // call database
    const results = this.db.call('query');
    return results; // this value is automatically logged
  }
  
  @bound
  methodThatRetainsThis(flagVal) {
    // Still knows what this is even when passed as a callback
    this.flag = flagVal;
  }
}

Similar to languages like Java and Dart, the decorators proposal uses an “@” symbol followed by the decorator name. The proposal champions have been working to align with the class fields proposal so that annotations can get used on classes, class methods, class fields, and likely private fields. This should lead to classes that are powerful yet easy to understand. If you want to learn more, check out our TypeScript Decorators blog post describing the original decorators proposal.

What needed to change? If I wrote a decorator, how would I update it?

The original proposal (which was implemented by Babel and TypeScript and leveraged by various framweworks) had a design that passed the descriptor and the target (e.g. the class being decorated) at one time. However, this design was prone to conflicts between decorators. If two decorators wanted to add a property to a class, one decorator would override the other. Additionally, the original proposal was written before private fields and methods entered the standardization process. There were other less significant changes which needed to be made as well, such as removing ambiguity in the decorator syntax that conflicted with calculated property names.

// decorator 1
function addMethod(target) {
  target.prototype.toString = () => 'addMethod decorator';
}
// decorator 2
function noIWantToAddTheMethod(target) {
  // This will be overridden
  target.prototype.toString = () => 'noIWantToAddTheMethod decorator';
}

@addMethod
@noIWantToAddTheMethod
class IncommingConflict {}

The updated proposal provides different phases for decorators to run. Decorators now return a descriptor API with optional initializer and finisher functions. This allows the language to throw an error if two decorators try to define the same descriptor. The new proposal also should enable creating and modifying private fields. However, these improvements require changes for existing decorator libraries and transpilers.

The original decorators proposal would pass the target object to the decorator function. The decorator could then modify the target in any manner. The new proposal still allows many changes but has a defined way to accomplish common changes. Any custom changes need to be made in either an initializer or a finisher function and the defined properties get passed via the descriptor.

function addLogger(descriptor) {
  const {
    kind, // This will be "class"
    elements // Array of all class elements
  } = descriptor;
  
  const updatedElements = elements.concat([{
    kind: 'field',
    placement: 'own',
    key: 'logger',
    descriptor: {}, // argument to Object.defineProperty
    initializer: () => (...args) => console.log('Custom Logger', ...args)
  }]);
  
  return {
    kind,
    elements: updatedElements,
    finisher: () => console.log('The class has been created')
}
  
// Create a bound version of the method as a field
// This is taken from the proposal
function bound(elementDescriptor) {
  let {
    kind, // This will be "method"
    key,  // Name of the method that this is being used on
    descriptor
  } = elementDescriptor;
  let {
    value // method as it was defined on the class
  } = descriptor
  function initializer() {
    return value.bind(this);
  }
  // Return both the original method and a bound function field that calls the method.
  // (That way the original method will still exist on the prototype, avoiding
  // confusing side-effects.)
  let boundFieldDescriptor = { ...descriptor, value: undefined }
  return {
    ...elementDescriptor,
    extras: [
      { kind: "field", key, placement: "own", descriptor: boundFieldDescriptor, initializer }
    ]
  }
}

@addLogger
class YayDecorators {
  @bound
  methodThatRetainsThis(flagVal) {
    // Still knows what this is even when passed as a callback
    this.flag = flagVal;
  }
  
}
  
const instance = new YayDecorators();
const boundMethod = instance.methodThatRetainsThis;
setTimeout(() => boundMethod(true), 1000);

I’m just using decorators. What changes do I need to make?

When migrating to the new decorators proposal, there are a few changes which you may encounter. The grammar for referencing decorators is now restricted. You cannot reference a decorator outputted by certain complex expressions such as a function call (e.g. @gen().decorator) or from a computed property accessor (e.g. @lib[variable]). However, you can easily access these more expressive syntaxes if you wrap the expression in parentheses (e.g. @(lib[variable])). For those using the legacy Babel decorators implementation, decorators on object properties were removed to match the proposal. The committee may consider adding this feature through a follow-up proposal.

The original decorators proposal placed the decorator before the exportexport. While not yet resolved, there is a high likelihood that the final proposal will settle on moving decorators after the export keyword. Not having the right placement will result in a syntax error, so be prepared to make a change.

Babel is the first implementor to update to use the latest version of the decorators proposal with the Babel 7 release and have published an update guide. In addition to updating the decorators transform, a Babel-based migration utility is available to wrap legacy decorators.

For those using decorators in TypeScript, you will need to continue using the experimentalDecorators flag until the decorators proposal gets finalized. Today the TypeScript team typically waits for proposals to stabilize at Stage 3 before implementing. The team also has not published its upgrade strategy yet so watch the TypeScript project roadmap for details. If you must update to the latest decorators proposal, you can integrate TypeScript into a Babel build to retain type checking.

What’s next?

The decorators proposal is still at stage 2, so the standard is still subject to change before becoming an official part of the JavaScript language. Due to a specification bugfix change, the earliest that the proposal would be likely to advance would be January 2019. Once it reaches stage 3, browsers and transpilers (like TypeScript) will work towards implementing the new standard. Libraries with decorators will need to release updates with standard-compliant decorators. The @ember-decorators library demonstrates concurrent support of both proposals. If other libraries adopt this pattern, it could help aid migration.

To stay current with the latest status of the proposal, watch the official decorators GitHub repo and the latest TC39 meeting notes. Keep an eye on the class fields proposal, which is already at stage 3, because many of its features are relied upon by the decorators proposal. While it has taken some time, the decorators proposal is continuing to progress and the season for updating decorators is coming.

If you’d like to know more about the decorators proposal or if you need help leveraging newer ES features within your application, feel free to reach out to us, and we’ll be more than happy to help!