Babel 7.1.0 finally supports the new decorators proposal: you can try it out by using the @babel/plugin-proposal-decorators
plugin 🎉.
A Bit of History
Decorators were first proposed by Yehuda Katz more than three years ago. TypeScript released support for decorators in version 1.5 (2015) alongside with many ES6 features. Some major frameworks, like Angular and MobX, started using them to enhance their developer experience: this made decorators popular and gave the community a false sense of stability.
Babel first implemented decorators in version 5, but removed them in Babel 6 because the proposal was still in flux. Logan Smyth created an unofficial plugin (babel-plugin-transform-decorators-legacy
) which replicated the Babel 5 behavior; it has since then been moved to the official Babel repository during the first Babel 7 alpha release. This plugin still used the old decorators semantics, because it wasn't clear yet what the new proposal would have been.
Since then, Daniel Ehrenberg and Brian Terlson become co-authors of the proposal along with Yehuda Katz, it has been almost completely rewritten. Not everything has been decided yet, and there isn't a compliant implementation as of today.
Babel 7.0.0 introduced a new flag to the @babel/plugin-proposal-decorators
plugin: the legacy
option, whose only valid value was true
. This breaking change was needed to provide a smooth transition path from the Stage 1 version of the proposal to the current one.
In Babel 7.1.0 we are introducing support for this new proposal, and it is enabled by default when using the @babel/plugin-proposal-decorators
plugin. If we didn't introduce the legacy: true
option in Babel 7.0.0, it wouldn't be possible to use the correct semantics by default (which would be equivalent to legacy: false
).
The new proposal also supports decorators on private fields and methods. We haven't implemented this feature yet in Babel (for each class, you can use either decorators or private elements), but it will come very soon.
What Changed In The New Proposal?
Even though the new proposal looks very similar to the old one, there are several important differences that make them incompatible.
Syntax
The old proposal allowed any valid left-hand side expression (literals, function and class expressions, new
expressions and function calls, simple and computed property accesses) to be used as the body of a decorator. For example, this was valid code:
class MyClass {
@getDecorators().methods[name]
foo() {}
@decorator
[bar]() {}
}
That syntax had a problem: the [...]
notation was used as both property access inside the decorator body and to define computed names. To prevent such ambiguity, the new proposal only allows dot property access (foo.bar
), optionally with arguments at the end (foo.bar()
). If you need more complex expressions, you can wrap them in parentheses:
class MyClass {
@decorator
@dec(arg1, arg2)
@namespace.decorator
@(complex ? dec1 : dec2)
method() {}
}
Object Decorators
The old version of the proposal allowed, in addition to class and class elements decorators, object members decorators:
const myObj = {
@dec1 foo: 3,
@dec2 bar() {},
};
Due to some incompatibilities with the current object literal semantics, they have been removed from the proposal. If you are using them in your code, stay tuned because they might be re-introduced in a follow-on proposal (tc39/proposal-decorators#119).
Decorator Functions Arguments
The third important change introduced by the new proposal is about the arguments passed to the decorator functions.
In the first version of the proposal, class elements decorators received a target class (or object), a key, and a property descriptor — similar in shape to what you would pass to Object.defineProperty
. Class decorators took as their only argument a target constructor.
The new decorators proposal is much more powerful: element decorators take an object which, other than changing the property descriptor, allows changing the key, the placement (static
, prototype
or own
), and the kind (field
or method
) of the element. They can also create additional properties and define a function (a finisher) which is run on the decorated class.
Class decorators take an object which contains the descriptors of every single class element, making it possible to modify them before creating the class.
Upgrading
Given these incompatibilities, it is not possible to use existing decorators with the new proposal: this would make the migration very slow, since existing libraries (MobX, Angular, etc.) can't be upgraded without introducing breaking changes. To workaround this issue, we have published an utility package which wraps the decorators in your code. After running it, you can safely change your Babel config to use the new proposal 🎉.
You can upgrade your files using a one-liner:
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --write
If your code only runs in Node, or if you are bundling your code with Webpack or Rollup, you can avoid injecting the wrapper function in each file by using an external dependency:
npm install --save decorators-compat
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --external-helpers --write
For more information, you can read the package documentation.
Open Questions
Not everything has been decided yet: decorators are a very big feature and defining them in the best possible way is complex.
Where Should Decorators on Exported Classes Go?
The decorator proposal has gone back and forth on this question: should decorators come before or after the export keyword?
export @decorator class MyClass {}
// or
@decorator
export class MyClass {}