• Start Date: 2019-02-06
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/440
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/41

Decorator Support

Summary

Move forward with the Decorators RFC via stage 1 decorators, and await a stable future decorators proposal.

Motivation

The recently merged Decorators RFC contained an explicit condition: That it was premised on decorators moving from stage 2 in the TC39 process to stage 3. If this were to not happen, the original RFC stipulates that a followup (this RFC) should be created to clarify whether or not we should move forward with decorators while they remain in stage 2, and how we would do so.

This RFC proposes that we do so. As discussed in the original, the decorator pattern has been in use in Ember since the beginning, and there is no easy path forward to native class syntax without a native equivalent to "classic" decorators.

Stage 2 in TC39 signals that the committee expects this feature to be included in the language in the future, and is working out the details and semantics. The fact that decorators remain in stage 2, and have not been rejected, signals that they still expect this to be the case. However, it is clear based on the initial work following the January TC39 meeting that the proposal could change significantly between now and stage 3.

Parts of the proposal, such as how decorators are invoked, seem solid based on the feedback we received at the January TC39 meeting, and based on the draft of the new spec. The definition of decorators is the most likely thing to change. As such, user code should be minimally affected by any changes, and most changes should be codemod-able. This reduces the risk of adopting decorators now, since code written with decorators shouldn't need to change that much.

The current recommendation from the authors of the spec is to use the stage 1 decorators proposal until the next iteration is ready. Stage 1 decorators are very stable, and used by a large community of developers outside of Ember. By standardizing on this version of the spec, we'll be able to gain the benefits of using a shared solution with the wider ecosystem while we wait for the next iteration.

Detailed design

Classic Classes

The first thing to note is that Ember.js will continue to work with classic classes and classic class syntax for the forseeable future, as a way for users to opt-out if they don't want to take the risk of using native classes. This includes:

  • Classic Classes
  • Classic Components
  • Mixins
  • Computed Properties
  • Observers
  • Tracked Properties
  • Injections
  • All existing classes defined with classic class syntax

Notably, GlimmerComponent will not support classic class syntax, due to its constraint of having to support both Ember.js and Glimmer.js, where the classic class model is not available. However, creating an API compatible classic class version using Ember's component manager API should be possible if users want to write Glimmer-like components with classic syntax.

Decorator Semantics

Ember will support Babel's stage 1 decorator transforms. Since Babel and TypeScript's decorators overlap so much, most TypeScript decorators should also work in Ember apps. However, due to subtle differences in the way Babel and TypeScript's decorators work, we can't support them both in all cases. In cases where there are nuanced differences, Babel's transforms will be considered the canonical source of truth. For ember-cli-typescript users this shouldn't be an issue, since they can use Babel's transforms in the latest versions of ember-cli-typescript.

Class Field Assigment Order

Ember also does not guarantee a particular ordering for the assignment of decorated fields in order to support users who want to use native class fields when they are available. All that Ember guarantees is that by the time the constructor's super method completes (or the constructor code begins, if the class is not extending), all class fields will be assigned.

Assigning class fields based on the values of other class fields is somewhat of an anti-pattern as is, so this would reinforce discouraging problematic patterns. To be clear:

class MyComponent extends Component {
  // This is ok, because the component instance exists
  // for all class fields.
  stateManager = new StateManager(this);

  @service time;

  // This is ok, because the `time` is an injection,
  // and always available for all class fields.
  createdAt = this.time.now();

  // This is problematic in general, not just with decorators, because
  // it relies on the order of class field assignment within this class.
  // It is a refactoring hazard, and should likely be done in the
  // constructor instead.
  registry = new Registry();
  container = new Container(this.registry);
}

Ember will attempt to provide a lint rule which can handle this level of nuance and prevent frustration when users copy and paste code around the class body.

Class Field Assignment Semantics

Currently, the Babel stage 1 transforms require loose mode for class fields which causes them to be assigned directly rather than using Object.defineProperty, which is not inline with the class fields spec. The biggest difference in behavior is when a child class attempts to override a getter/setter on the parent class:

class Foo {
  get baz() {
    return this._baz;
  }

  set baz(value) {
    this._baz = value;
  }
}

class Bar extends Foo {
  baz = 123;
}

In strict mode, baz in the child class would completely override the getter/setter, and they would not exist/be usable on an instance of Bar (except through calls to super). In loose mode, baz in the child class would go through the getter/setter, and be assigned to _baz on the instance.

In order to mitigate this, Ember will provide an assertion in development builds via a babel transform for fields that are assigned in a non-spec compliant way that throws in this scenario. This will prevent users from accidentally writing code that will be hard to migrate forward to class fields in the future.

Decorator Support Timeframe

Stage 1 decorators will be considered a first class Ember API. They will be supported until:

  1. A new RFC has been made to introduce a new decorator API as a parallel API to the stage 1 decoraters.
  2. A deprecation RFC has been created for Stage 1 decorators.

They will follow the same deprecation flow, and same SemVer requirements, as any other Ember feature.

How we teach this

There are a few different important aspects of this that should be taught:

  1. Class Field and Decorator semantics, specifically around the ordering of class fields. While any step to change a user's class field ordering would likely be based on their target browsers and configuration, and would be unlikely to change in a patch or minor version release, users writing code that could be difficult to upgrade or dependent on a particular version of the class field/decorator transforms is a failure case we want to avoid.

  2. Support timeframe and alternatives. Users should be aware that this will be a feature that will likely change in the future, if only subtly. We should highlight that there is an alternative, classic class syntax, and that it will still be fully supported. Ultimately, this point is about setting expectations - that a year or so from now, when decorators are truly final, there will be another shift.

  3. Custom Decorators. We should document how users can write their own decorators, but also caution against behaviors that will be difficult to replicate in Stage 3 decorators when they are ready. This will necessarily be a moving target, since stage 3 decorators are not yet finished, but we can try our best to recommend against usages that could be problematic.

    We should also warn that decorator definition code will almost definitely have to be rewritten for stage 3. This should be very clear, so that users understand that adopting custom decorators in their apps means taking on more tech debt than just using official Ember decorators, and decorators provided by addons.

Drawbacks

  • The fact that decorators did not move to stage 3 signals that there may be additional changes to the spec in the near future. Adopting them now could cause more churn than it's worth.

  • By adopting decorators now, we are essentially taking on some amount of debt that we know will have to be repaid in the future as a community. This is less than ideal, since we know that standardization is coming, it's just a matter of when. This is also the situation we've been in for quite some time already, and with the latest turn of events, it seems unlikely that decorators will be fully standardized within the year.

    We can continue to wait, but there is no deadline on the design process, and we could be stuck here indefinitely. This move is pragmatic in that it unblocks us for now, and moves us toward using modern syntax that will be compatible with stage 3 decorators - it allows us to begin unwinding other layers of technical debt. It also represents minimal risk and debt to users of decorators, since the invocation style will likely be the same.

Alternatives

  • We could continue to rely on unofficial addons such as ember-decorators, which allow users to opt-in to using decorators. However, these libaries have limitations that first class decorators will not have, and they don't allow us to update the official guides to use them.

  • We could create an official decorators addon. However, this means that decorators would be available at a different import path, meaning that any code which seeks to work with both classic classes and native classes would have to be written twice. This would be very difficult for applications that are mid-transition, and even more difficult throughout the ecosystem for addon authors.