JavaScript-TypeScript — Decorators

What is a Decorator?

A decorator is simply a way of wrapping a function with another function to extend its existing capabilities. You “decorate” your existing code by wrapping it with another piece of code. This concept will not be new to those who are familiar with functional composition or higher-order functions.

With the introduction of Classes in TypeScript and ES6, there now exist certain scenarios that require additional features to support annotating or modifying classes and class members. Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.

Why Use a Decorator?

Decorators allow you to write cleaner code and achieve composition. It also helps you extend the same functionality to several functions and classes. Thereby enabling you to write code that is easier to debug and maintain.

Decorators also allow your code to be less distracting as it removes all the feature enhancing code away from the core function. It also enables you to add features without making your code complex.

Being in the stage 2 proposal, there can be many additions to the class decorator proposal which can be beneficial.

Lets us walk through the decorators:

→ Decorators always work with class

→ This is a feature applied to a class, which will help others while coding.

→ There are many frameworks that use this concept i.e. Angular, Nest JS (Server-side JS), JavaScript, Redux.

STEP1: Before using decorators, the first step is to enable it in the TS configuration file i.e. tsconfig.ts file

"target": "es6", "experimentalDecorators": true,

A decorator is a regular function, which expects some arguments. The first letter should be capital (TS standard).

→ The decorator function should be written before the class definition.

A decorator is called by @<FunctionName> i.e. @Logger (must not invoke it)

  1. First Decorator (Class Decorator)

Let us see the below example:

// Decorator Function
function Logger(constructor: Function) {
console.log("Logger...."); console.log(constructor);}@Loggerclass Person { name = "Amir"; constructor() { console.log("Constructor called.."); }}const pers = new Person();console.log(pers);

→ Here our decorator o/p is printed first (i.e. function called before class)

→ Decorators execute when the class is defined and not when its instance is created.

2. Decorator Factory:

→ A factory simply means creator of something

→ The decorator factory returns the decorator function with some additional data. It is a function that returns decorator functions.

→ It is powerful because we can pass additional data/configuration to decorator functions.

Eg.

function Logger5(status: boolean = false, data:number = null) {  return function (constructor: Function) {   if(status) {
// Some logic..
console.log("Logger...."); console.log(anydata); console.log(constructor);
}
};}const status = true;const data = 100;@Logger5(status, data) // Passing some dataclass Person5 { name = "Amir"; constructor() { console.log("Constructor called.."); }}const pers5 = new Person5();console.log(pers5);

3. Building more useful Decorators:

→ Suppose we have a template library and we want to import it in the component or ts file. Let us see this example

Eg. app.ts

// Decorator Factory
function WithTemplate(template: string, hookId: string) {
return function (_: Function) { // _ tells TS to ignore const hookEl = document.getElementById(hookId); if (hookEl) { hookEl.innerHTML = template; } };}@WithTemplate("<h2>My Person Object</h2>", "app")class Person5 { name = "Amir"; constructor() { console.log("Constructor called.."); }}const pers5 = new Person5();console.log(pers5);

index.html

<!DOCTYPE HTML>  <html>    <head>      <title>My test page</title>      <script src="dist/app.js" defer></script>    </head>    <body>       <h1 id="app"></h1>    </body></html>

→ Suppose there is a requirement to use class data. Let us see in below example:

function WithTemplate(template: string, hookId: string) {  return function (constructor: any) {    const hookEl = document.getElementById(hookId);    const p = new constructor();  // Class instance created    if (hookEl) {      hookEl.innerHTML = template + p.name;  // Accessing class data    }  };}@WithTemplate("<h2>My Person Object</h2>", "app")  class Person6 {    name = "Amir";    // data    constructor() {       console.log("Constructor called..");    }  }const pers6 = new Person6();console.log(pers6);

→ Now that we know the decorator, let us see a popular JS framework Angular

a. Angular Decorator

Angular Decorator

→ This is also somewhat similar to our code, it takes template and path. Angular does this in a more advanced way.

→ The important thing to note is decorators work with a class that we already know. From the above screen, we can see, Decorators is used on top of AppComponent class.

b. Nest JS

→ Nest JS is a server-side JS framework that also uses decorators.

3. Adding Multiple Decorators:

→ We can add multiple decorators

→ The trick is to know the order in which it is called.

First function (parent function) -> factories

Inner function (i.e. child function) → decorators

Suppose there are three decorators factories in the below order called:

order in which factory and decorators called

→ Factories are called top to bottom

→ Decorators are called a bottom to top in the order they are called

Eg.

// Decorator Factoryfunction Logger7() {  console.log("1. LOGGER: Factory");  // Decorator Function  return function (constructor: Function) {    console.log("2. LOGGER: Decorator");    console.log(constructor);  };}// Decorator Factoryfunction WithTemplate7(template: string, hookId: string) {  console.log("3. TEMPLATE: Factory");  // Decorator Function  return function (constructor: any) {    console.log("4. TEMPLATE: Decorator");    const hookEl = document.getElementById(hookId);    const p = new constructor();    if (hookEl) {      hookEl.innerHTML = template + p.name;    }  };}// Decorator Call@Logger7()@WithTemplate7("<h2>My Person Object</h2>", "app")class Person7 {  name = "Amir";  constructor() {    console.log("Constructor called..");  }}const pers7 = new Person7();console.log(pers7);

Let is the order in which above logs will print

In the above example, we have seen decorator’s in-class i.e. Class Decorators.

Decorators can also be applied to a class’s property, accessor, methods, and parameters.

A. Decorators on Property (eg. title):

→ Let us walk through another example:

Eg.

// Property Decorator
function Log(target: any, propertyName: string | Symbol) {
console.log("Property decorator"); console.log(target, propertyName);}class Product { @Log // Log decorator is for string property title: string; private _price: number; set price(val: number) { if (val > 0) { this._price = val; } else { throw new Error("Invalid price - should be positive"); } } constructor(t: string, p: number) { this.title = t; this._price = p; } getPriceWithTax(tax: number) { return this._price * (1 + tax);
}
}

Decorator function attribute for property:

target → It is a function with prototype

propertname — the name of the property. (can be any name)

NOTE:

a. Decorators are called at the time of class defined

b. The decorator must be written above the specific property you want to call

B. Decorators on Accessor (eg. setter):

→ Getter and setters are examples of accessors

Eg.

// Property Decorator
function Log(target: any, propertyName: string | Symbol) {
console.log("Property decorator"); console.log(target, propertyName);}// Accessor Decoratorfunction Log2(target: any, name: string, descriptor: PropertyDescriptor) { console.log("Log2 ======>", "Property Descriptor"); console.log("target", target); console.log("name", name); console.log("descriptor", descriptor);}class Product { @Log title: string; private _price: number; @Log2 // With Accessor set price(val: number) { if (val > 0) { this._price = val; } else { throw new Error("Invalid price - should be positive"); }}

Decorator function attribute for accessor:

target — It is a function with prototype

name — name of function

descriptor — object similar to object defined property.

i.e. {get: undefined, enumerable: false, configurable: true, set: ƒ}

C. Decorators on Methods:

We can also add decorators before methods. Let us see the below example:

function Log(target: any, propertyName: string | Symbol) {  console.log("Log1 ======>", "Property Decorator");  console.log("target", target);  console.log("name", propertyName);  console.log("--------------------------------------------");}function Log2(target: any, name: string, descriptor: PropertyDescriptor) {  console.log("Log2 ======>", "Accessor Decorator");  console.log("target", target);  console.log("name", name);  console.log("descriptor", descriptor);  console.log("--------------------------------------------");}// Method Decoratorfunction Log3(target: any, name: string | Symbol, descriptor: PropertyDescriptor) {  console.log("Log3 ======>", "Method Decorator");  console.log("target", target);  console.log("name", name);  console.log("descriptor", descriptor);  console.log("--------------------------------------------");}class Product {  @Log    // Property Decorator (i.e. for string)  title: string;  private _price: number;  @Log2  // Access Decorator (i.e. for _price)  set price(val: number) {    if (val > 0) {      this._price = val;    } else {      throw new Error("Invalid price - should be positive");    } }  constructor(t: string, p: number) {    this.title = t;    this._price = p;  }  @Log3 // Method Modifier (i.e. for getPriceWithTax)  getPriceWithTax(tax: number) {    return this._price * (1 + tax);  }}let product = new Product("Desk", 100);

Decorator function attribute for accessor:

target — It is a function with prototype

name — name of function

descriptor — This object is similar to objectDefine property.

D. Decorators on Parameters:

→ We can add a decorator to a parameter as well.

→ Separate decorators can be added per parameter as per requirement.

Let us see what arguments this decorator accepts:

function Log4(target: any, name: string | Symbol, position: number){  console.log("Log4 ======>", "Paramater Decorator");  console.log("target", target);  console.log("name", name);  console.log("position", position);  console.log("----------------------------- ---------------");}
class Product {
title: string; private _price: number; getPriceWithTax(@Log4 tax: number) { return this._price * (1 + tax); }}let product = new Product("Desk", 100);

Decorator function attribute for parameter:

target — It is a function with prototype

name — name of function in which parameter is applied

position — object similar to object defined property. (say paramter 0)

TRICK:

All decorators almost work the same. The important thing is to note:

When the decorator is called

2. Which parameter is receieved when that decorator is called

1. Property Decorators — receive two arguments

i.e. target: any, propertyName:string | Symbol

2. Accessor Decorators — receive three arguments

i.e. target: any, name: string, descriptor: PropertyDescriptor

3. Method Decorators — receive three arguments

i.e. target: any, name: string, descriptor: PropertyDescriptor

4. Parameter Decorators — receive three arguments

i.e. target: any, name: string, position: number

5. Class Decorator — receive one argument (one written right outside class)

i.e. constructor: Function

When is the Decorator called?

→ Decorator is called once when class is defined

→ Decorator is not called on class instance created

→ We can write some meta-programming to make it on class instance.

let is see below cases

A. Returning and changing a class in a Class decorator:

→ A decorator can also extend a class and add some functionality

→ For this the decorator function can itself return the function.

→ For class decorator and method decorator this can happen

→ In the below example we see, decorator function itself is returning an anonymous class (or constructor function)

function Template(template: string, hookId: string) {  return function <T extends { new (...args: any[]): { name: string } }>(originalConstructor: T) {  return class extends originalConstructor { // anonymousclass retrn    constructor(..._: any[]) {    //_ ts will know, that it gets         parameter but will not use it       super();       const hookEl = document.getElementById(hookId);       if (hookEl) {        hookEl.innerHTML = template;        hookEl.querySelector("h3")!.innerHTML += this.name;       }    }  }; };}// Class Decorator
@Template("<h3>My template</h3>", "app")
class Productt { name = "Amir"; constructor() { console.log("Constructor called.."); }}let productt = new Productt(); // instantiating is must for constructor to run

→ So now if we remove the instantiating above line, we will see decorator template will go away.

→ This we have done meta-programming and instead of calling in the class definition, we are calling in the class instantiation

→ Here in the class decorator, we returned a new class that has current class logic + some custom logic

B. Returning and changing a class in a Method/Accessor decorator

→ We can return the method to the class

Let us see the example step by step

Eg 1:

index.html

<!DOCTYPE HTML>  <html>  <head>    <title>My test page</title>    <script src="dist/app.js" defer></script>  </head>  <body>    <h3 id="app">Typescript</h3>    <button>Click me!</button>  </body></html>

app.ts

class Printer {  message = "This works";  showMessage() {    console.log(this.message);  }}const p = new Printer();const btn = document.querySelector("button")!;btn.addEventListener("click", p.showMessage);  // O/p is undefined as this is not pointing to class

→ We see the o/p is undefined. It is because this pointer is not pointing correctly

Eg1.2 quick fix is adding — bind()

class Printer {  message = "This works";  showMessage() {    console.log(this.message);  }}const p = new Printer();const btn = document.querySelector("button")!;btn.addEventListener("click", p.showMessage.bind(p));  // O/p This works

Eg1.3: With Decorator:

Suppose there are many classes, and calling is done. in many places. Being developers we should not write in every place.

If a class can handle itself, it would be neat. A decorator can help us here.

→ the third attribute of method decorator is property descriptor

→ value property inside it points to a functional

→ We will bind there

→ Let us create one @Autobind named decorator

function Autobind(_target: any,_methodname: string,descriptor: PropertyDescriptor) {  const originalMethod = descriptor.value;  const adjDescriptor: PropertyDescriptor = {    configurable: true,    enumerable: false,    // removed value, added our getter which will bind
get
() {
const boundFn = originalMethod.bind(this); // this inside get points to obj return boundFn; }, }; return adjDescriptor;}class Printer { message = "This works"; @Autobind showMessage() { console.log(this.message); }}const p = new Printer();const btn = document.querySelector("button")!;btn.addEventListener("click", p.showMessage);

There are many built-in decorators which makes development faster.

  1. Class Validator: https://github.com/typestack/class-validator

It has built-in methods which can be used in a fly.

2. Angular decorators:

If anyone is familiar with typescript and Angular, they would have definitely come across decorators being used within Angular classes. You can find decorators such as “@Component”, “@NgModule”, “@Injectable”, “@Pipe” and more. These decorators come loaded built-in and decorate the Class.

Angular Decorator

3. Nest JS Decorators:

4. Redux Library in React

The Redux library for React contains a connect the method that allows you to connect a React component to a Redux store. The library allows for the connect method to be used as a decorator as well.

//Before decorator
class MyApp extends React.Component {
// ...define your main app here
}
export default connect(mapStateToProps, mapDispatchToProps)(MyApp);//After decorator
@connect(mapStateToProps, mapDispatchToProps)
export default class MyApp extends React.Component {
// ...define your main app here
}

Felix Kling’s Stack Overflow answer explains this.

Furthermore, although connect supports the decorator syntax, it has been discouraged by the redux team at the moment. This is mostly because the decorator proposal in stage 2 can accommodate changes in the future.

5. JavaScript

This JavaScript library provides readymade decorators out of the box. Although this library is based on the stage 0 decorator proposal, the author of the library is waiting until the proposal reaches stage 3, to update the library.

This library comes with decorators such as “@readonly”, “@time”, “@deprecate” and more. You can check out more over here.

Closing thoughts:

Decorators are a powerful tool that enables you to create code that is very flexible. There is a very high chance you will come across them quite often in the near future.

Thank you for being till the end 🙌 . If you enjoyed this article or learned something new, support me by clicking the share button below to reach more people and/or give me a follow on Twitter to see some other tips, articles, and things I learn and share there.

Web Artisan. Human — Engineering