JavaScript Decorators is a powerful feature that finally reached stage 3 and is already supported by Babel.
Here you can read the official proposal https://github.com/tc39/proposal-decorators.
I want to guide you on seamlessly activating the latest decorator feature support for your application. I made a straightforward plan to dive into the decorator feature.
Predefine environment
In order to try the latest feature of decorators, you can create .babelrc file
{
"presets": [
["@babel/env", {
"targets": {
"node": "current"
}
}]
],
"plugins": [
["@babel/plugin-proposal-class-static-block"],
["@babel/plugin-proposal-decorators", { "version": "2022-03" }],
["@babel/plugin-proposal-class-properties", { "loose": false }]
]
}
And here is the package.json, what you can take as an example:
{
"scripts": {
"build": "babel index.js -d dist",
"start": "npm run build && node dist/index.js"
},
"devDependencies": {
"@babel/cli": "^7.19.3",
"@babel/core": "^7.20.5",
"@babel/plugin-proposal-decorators": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-stage-3": "^7.8.3"
}
}
Introduction
Probably you are already familiar with Typescript decorators, and you may see it somewhere, it looks like "@myDecorator". Declared decorators start from "@" and can be applied for classes, methods, accessors, and properties.
Here are some examples you may see before:
function myDecorator(value: string) {
// this is the decorator factory, it sets up
// the returned decorator function
return function (target) {
// this is the decorator
// do something with 'target'
return function(...args) {
return target.apply(this, args)
}
};
}
class User {
@myDecorator('value')
anyMethod() { }
}
Javascript supports natively decorators, which haven’t been part of standard ECMAScript yet but are experimental features. Experimental means it may be changed in future releases. That is what happened. Actually, the latest proposal of the decorators introduced a new syntax and more accurate and purposeful implementation.
Decorators in Typescript
Javascript has been introducing decorators since 2018. And TypeScript support decorators as well, with "experimentalDecorators" enabled option
{
"compilerOptions": {
...
"experimentalDecorators": true
}
}
Notice
TypeScript doesn’t support the last proposal of decorators
https://github.com/microsoft/TypeScript/pull/50820
Nevertheless Typescript provides enough powerful and extended functionality around decorators:
- Decorator Composition (we can wrap few times an underlying function)
class ExampleClass {
@first()
@second()
method() {}
}
It is equal to:
first(second(method()))
// or the same as
const enhancedMethod = first(method());
const enhancedTwiceMethod = second(enhancedMethod())
- Parameter Decorators (Unlike JS, TS decorators support decorating params)
class User {
getRole(@inject RBACService) {
return RBACService.getRole(this.id)
}
}
The feature is included in the plans for Typescript version 5.0.
How do decorators work?
The decorator basically high order functions. It's a kind of wrapper around another function and enhances its functionality without modifying the underlying function.
Here example of the legacy version:
function log(target, name, descriptor) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function(...args) {
console.log(`Arguments: ${args}`);
try {
const result = original.apply(this, args);
console.log(`Result: ${result}`);
return result;
} catch (e) {
console.log(`Error: ${e}`);
throw e;
}
}
}
return descriptor;
}
And the new implementation (that became stage-3 of the proposal):
function log(target, { kind, name }) {
if (kind === 'method') {
return function(...args) {
console.log(`Arguments: ${args}`);
try {
const result = target.apply(this, args);
console.log(`Result: ${result}`);
return result;
} catch (e) {
console.log(`Error: ${e}`);
throw e;
}
}
}
}
class User {
@log
getName(firstName, lastName) {
return `${firstName} ${lastName}`
}
}
As you can see there are new options, we don't use descriptors anymore to change an object, but we got closer to metaprogramming approach. I really like how Axel Rauschmayer describes what metaprogramming is:
- We don’t write code that processes user data (programming).
- We write code that processes code that processes user data (metaprogramming).
Let's take a look closer at the new signature of decorators, here is the new type well described in TS (but hasn’t merged to master yet), we will use it just as an example
type Decorator = (
value: DecoratedValue,
context: {
kind: string;
name: string | symbol;
addInitializer(initializer: () => void): void;
// Don’t always exist:
static: boolean;
private: boolean;
access: {get: () => unknown, set: (value: unknown) => void};
}
) => void | ReplacementValue;
Kind parameter can be:
'class'
'method'
'getter'
'setter'
'accessor'
'field'
Kind property tells the decorator which kind of JavaScript construct it is applied to.Name is the name of a method or field in a class.
addInitializer allows you to execute code after the class itself or a class element is fully defined.
Auto-accessors
The decorators' proposal introduces a new language feature: auto-accessors.
To understand what the auto-accessors feature is, let's take a look at the below example:
// New "accessor" keyword
class User {
accessor name = 'user';
constructor(name) {
this.name = name
}
}
// it's the same as
class User {
#name = 'user';
constructor(name) {
this.name = name
}
get name() {
return this.#name;
}
set name(value) {
this.#name = value;
}
}
Auto-accessor is a shorthand syntax for standard getters and setters that we get used to implementing in classes.
We can apply decorators for accessors easily with the new proposal:
function accessorDecorator({get,set}, {name, kind}) {
if (kind === 'accessor') {
return {
init() {
return 'initial value';
},
get() {
const value = get.call(this);
...
return value;
},
set(newValue) {
const oldValue = get.call(this);
...
set.call(this, newValue);
},
};
}
}
class User {
@accessorDecorator
accessor name = 'user';
constructor(name) {
this.name = name
}
}
Fields Decorator
How can we decorate fields by legacy approach?
Here code example:
function fieldDecorator(target, name, descriptor) {
return {
...descriptor,
writable: false
initializer: () => 'EU'
}
}
class Customer {
@fieldDecorator
country = 'USA';
getCountry() {
return this.country
}
}
const customer = new Customer('john')
customer.getCountry(); // 'EU' instead of USA, because of initializer
customer.country = 'DE' // TypeError: Cannot assign to read only property 'country' of object '#<Customer>'
Limitation legacy decorators:
- We can't decorate private fields
- it’s always hacky to decorate efficiently on accessors of fields
The new proposal is more flexible:
function readOnly(value, {kind, name}) {
if (kind === 'field') {
return function () {
if (!this.readOnlyFields) {
this.readOnlyFields = []
}
this.readOnlyFields.push(name)
}
}
if (kind === 'class') {
return function (...args) {
const object = new value(...args);
for (const readOnlyKey of object.readOnlyFields) {
Object.defineProperty(object, readOnlyKey, { writable: false });
}
return object;
}
}
}
@readOnly
class Customer {
@readOnly
country;
constructor(country) {
this.country = country
}
getCountry() {
return this.country
}
}
const customer = new Customer();
customer.getCountry(); // 'USA'
customer.country = 'EU' // // TypeError: Cannot assign to read only property 'country' of object '#<Customer>'
As you can see, we don't have access to the property descriptor. Still, we can implement it differently, collect all the not writable fields, and set "writable: false" through the class decorator.
Conclusion
I think this is a whole new level, as developers can dive even more into the world of metaprogramming and look forward to the release of Typescript 5.0 and when the new proposal becomes part of the EcmaScript standard.
Follow me on 🐦 Twitter if you want to see more content.
Top comments (0)