Metaprogramming in JavaScript/TypeScript Part #2

Daniel Ostrovsky
4 min readSep 10, 2022

--

This is the second article in the “Metaprogramming in JavaScript and TypeScript” series. It is highly recommended that you read the first part too:

In this article, we will talk about Decorators and why it’s so great!!

Decorators an essential part of metaprogramming in both JS and TypeScript. Python (and beyond) developers are very familiar with this pattern. At this point, officially, decorators are not yet part of JavaScript. For now, they are in the third stage of the proposal, meaning that decorators will become a language standard sooner or later.

However, there is no need to wait! We can now use decorators in JS (thanks to Babel) and TypeScript.

In this article, we will see the possibilities and examples of using decorators.

What is a decorator?

A decorator is nothing but a higher-order function (HoF). Namely, a function that takes another function as a parameter and returns it as a result. In the process, the HoF can change the resulting function, perform additional actions unrelated to the original function, add functionality, and much more.

This is what the decorator looks like in ES6 (syntax):

function myDecorator(target, key, descriptor) {
//code goese here
}

class Apartment {
constructor(apt) {
this.apt = apt;
}

@myDecorator
getFloor() {
return this.apt.floor;
}
}
  • @ is an indicator to show the parser that it is dealing with a decorator.
  • myDecorator” — the name of our High-Order Function

The “myDecorator” function takes three parameters:

  • target<Object>: the object of the method being decorated, in our case, the Apartment class.
  • key<string>: the name of the method or property we are decorating is getFloor
  • descriptor<Object descriptor>: a descriptor for our method/property. Read more about this in the first article.

Let’s move on to examples.

We will start with a relatively simple message about using deprecated methods.

function deprecator(target, key, descriptor) {
console.warn(`WARNING!! method ${key} is about to be deprecated!!`)
}
class Apartment {
constructor(apt) {
this.apt = apt;
}
@deprecator
getFloor() {
return this.apt.floor
}
}
// output WARNING!! method getFloor is about to be deprecated!!

Note that the message is displayed, although neither the “Apartment” class nor the decorated “getFloor” method is used anywhere. This means that the decorator is triggered when the code is compiled inside the JavaScript engine.

What if you need to pass a parameter to the decorator? In our case, whose method name should be used instead of the deprecated ones? To do this, you need to wrap the decorator in another function that will return the decorator function itself.

function deprecator(newMethod) {
return function (target, key, descriptor) {
console.warn(`WARNING!! method ${key} is about to be deprecated, instead use ${newMethod}`)
}
}
class Apartment {
constructor(apt) {
this.apt = apt;
}
@deprecator('newGetFloor')
getFloor() {
return this.apt.floor
}
newGetFloor() {
return this.apt.floor
}
}
// output WARNING!! method getFloor is about to be deprecated, instead use newGetFloor

Now our decorator can receive receptive parameters, and (target, key, descriptor) are passed to functions that are returned to our decorators.

Consider another common type of decorator — a logger. We are interested in the log every time the method is called.

let apt = {
floor: 12,
};

function logger(target, key, descriptor) {
let originalMethod = descriptor.value;
descriptor.value = function () {
console.log(`Method ${key} fired`);
return originalMethod.apply(this, arguments);
};
}

class Apartment {
constructor(apt) {
this.apt = apt;
}

@logger
getFloor() {
return this.apt.floor;
}
}

let myApt = new Apartment(apt);
console.log(myApt.getFloor());

Let’s take a look at the logger decorator.

function logger(target, key, descriptor) {
// save original (decorated) method
let originalMethod = descriptor.value;
// Replace with our function
descriptor.value = function () {
console.log(`Method ${key} fired`); // Log
// Call the original function
return originalMethod.apply(this, arguments);
};
}

Please note that our decorator has access to the arguments passed to the method being decorated. They can be analyzed, logged, validated, simply replaced, and much more.

An example of a timer decorator that will measure the execution time of a method.

function timer(target, key, descriptor) {
let originalMethod = descriptor.value;
descriptor.value = function () {
const t1 = Date.now();
const result = originalMethod.apply(this, arguments);
console.log(`Method ${key} took `, Date.now() - t1);
return result
}
}

Some useful household decorators can be found here:

https://github.com/jayphelps/core-decorators

Intermediate result:

Now we know what descriptors and decorators are. It’s time to move into a practical understanding of metaprogramming.

If you like this article, press the 👏 clap button ♾️ times.
Follow me on Twitter and Medium for blog updates.
Check out my website and Youtube for videos and public talks.

Feel free to ask a question.

Thanks a lot for reading!

--

--

Daniel Ostrovsky

Web development expert and teams manager with over twenty years experience in the industry.