JavaScript Decorators is a powerful feature that finally reached stage 3 and is already supported by Babel.
Here you can read the official proposalhttps://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:
functionmyDecorator(value:string){// this is the decorator factory, it sets up// the returned decorator functionreturnfunction(target){// this is the decorator// do something with 'target'returnfunction(...args){returntarget.apply(this,args)}};}classUser{@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)
classExampleClass{@first()@second()method(){}}
It is equal to:
first(second(method()))// or the same asconstenhancedMethod=first(method());constenhancedTwiceMethod=second(enhancedMethod())
- Parameter Decorators (Unlike JS, TS decorators support decorating params)
classUser{getRole(@injectRBACService){returnRBACService.getRole(this.id)}}
The feature is included in the plans forTypescript 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:
functionlog(target,name,descriptor){constoriginal=descriptor.value;if(typeoforiginal==='function'){descriptor.value=function(...args){console.log(`Arguments:${args}`);try{constresult=original.apply(this,args);console.log(`Result:${result}`);returnresult;}catch(e){console.log(`Error:${e}`);throwe;}}}returndescriptor;}
And the new implementation (that became stage-3 of the proposal):
functionlog(target,{kind,name}){if(kind==='method'){returnfunction(...args){console.log(`Arguments:${args}`);try{constresult=target.apply(this,args);console.log(`Result:${result}`);returnresult;}catch(e){console.log(`Error:${e}`);throwe;}}}}classUser{@loggetName(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 tometaprogramming 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
typeDecorator=(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" keywordclassUser{accessorname='user';constructor(name){this.name=name}}// it's the same asclassUser{#name='user';constructor(name){this.name=name}getname(){returnthis.#name;}setname(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:
functionaccessorDecorator({get,set},{name,kind}){if(kind==='accessor'){return{init(){return'initial value';},get(){constvalue=get.call(this);...returnvalue;},set(newValue){constoldValue=get.call(this);...set.call(this,newValue);},};}}classUser{@accessorDecoratoraccessorname='user';constructor(name){this.name=name}}
Fields Decorator
How can we decorate fields by legacy approach?
Here code example:
functionfieldDecorator(target,name,descriptor){return{...descriptor,writable:falseinitializer:()=>'EU'}}classCustomer{@fieldDecoratorcountry='USA';getCountry(){returnthis.country}}constcustomer=newCustomer('john')customer.getCountry();// 'EU' instead of USA, because of initializercustomer.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:
functionreadOnly(value,{kind,name}){if(kind==='field'){returnfunction(){if(!this.readOnlyFields){this.readOnlyFields=[]}this.readOnlyFields.push(name)}}if(kind==='class'){returnfunction(...args){constobject=newvalue(...args);for(constreadOnlyKeyofobject.readOnlyFields){Object.defineProperty(object,readOnlyKey,{writable:false});}returnobject;}}}@readOnlyclassCustomer{@readOnlycountry;constructor(country){this.country=country}getCountry(){returnthis.country}}constcustomer=newCustomer();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)
For further actions, you may consider blocking this person and/orreporting abuse