Metaprogramming in JavaScript/TypeScript Part #1

Descriptor.

Daniel Ostrovsky
4 min readMay 15, 2019

I would like to present to you a series of mini-articles, which describe the metaprogramming basics and techniques. Generally, I will write about the use of certain techniques in JavaScript or TypeScript

This is the first article in the series

So, what is metaprogramming?

JavaScript / TypeScript meetaprogramming

Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running. In some cases, this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time.

The definition is pretty complex, but the main benefit of metaprogramming is quite clear:

“… this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time”.

Actually, metaprogramming has so many forms of practical application, that the discussion on “where metaprogramming ends and the programming begins” might never get an accurate answer.

I have formulated the following rules for myself:
1) Metaprogramming should not define, change or affect the business logic by any means.
2) If the metaprogramming-related code is removed, the program should not be seriously affected.

Descriptor

In JavaScript, metaprogramming is a fairly new trend, having the descriptor as one of its fundamentals.

Descriptor is a kind of description (meta information) of a given property/method in an object.

Understanding and correctly application of descriptors will allow you a lot more than just create and modify methods or properties in your objects.

descriptors are also helpful in understanding how to operate with decorators, which is the topic of my next article.

Let’s take an apartment and describe it as an object, with it’s own properties:

let apt = {
floor: 12,
number: '12B',
size: 3400,
bedRooms: 3.4,
bathRooms: 2,
Price: 400000,
amenities: {...}
};

Let’s determine which properties can and cannot be changed.

For example, it is impossible to change the floor or the total size of the apartment, while the number of rooms or bathrooms can be changed.
As a result, we have the following requirements: to make it impossible to change the floor and size properties in apt objects.

To solve this task, we will need descriptors for each of these properties. To get the descriptor, use the static method getOwnPropertyDescriptor, which belongs to the Object class.

let descriptor = Object.getOwnPropertyDescriptor(apt, ‘floor’); console.log(descriptor);// Output
{
value: 12,
writable: true,
enumerable: true,
configurable: true
}

Let’s take a closer look at the next:

value<any> — this is the value assigned to the floor property at a certain moment
writable<boolean> — defines whether it is possible to change the value
enumerable<boolean> — defines if the floor property can or can’t be enumerated — (more on this later).
configurable<boolean> — defines the ability to make changes on the descriptor object.

To prevent the floor property from changing after initialisation, it is necessary to set the writable value to false.

The static defineProperty method is used to change descriptor properties. Its input parameters are the object itself (apt), property name (‘floor’) and the descriptor.

Object.defineProperty(apt, ‘floor’, {writable: false});

In this case, we pass not the entire descriptor object, but only the writable property having the false value.

Now let’s try to change the floor property value:

apt.floor = 44;
console.log(apt.floor);
// output
12

The value has not changed, and when using the ‘use strict’, an error message will be shown:

” Cannot assign to read only property ‘floor’ of object ‘…

Now it is impossible to change the value. However, we can still revert the writable to true and modify the floor property value. To avoid this, we have to set the configurable property value to false

Object.defineProperty(apt, ‘floor’, {writable: false, configurable: false});

Let’s make one more attempt to change the value of any of the property of our descriptor…

Object.defineProperty(apt, ‘floor’, {writable: true, configurable: true});

We get the next:

“TypeError: Cannot redefine property: floor…”

Let’s sum it up:

To make the value of the property unchangeable, it is necessary to specify the configuration of this property: {writable: false, configurable: false}.

//This can be done when initialising the property:
Object.defineProperty(apt, ‘floor’, {value: 12, writable: false, configurable: false});
//Or after the initialisation:
Object.defineProperty(apt, ‘floor’, {writable: false, configurable: false});

In the end, let’s consider an example with a class:

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

getFloor() {
return this.apt.floor
}
}

let apt = {
floor: 12,
number: '12B',
size: 3400,
beds: 3.4,
baths: 2.
};

Let’s change the getFloor method:

Apartment.prototype.getFloor = () => {
return 44.
};
let myApt = new Apartment(apt);
console.log(myApt);
// output will be changed.
44

Now let’s change descriptor of the getFloor method

Object.defineProperty(Apartment.prototype, 'getFloor',
{writable: false, configurable: false});
Apartment.prototype.getFloor = () => {
return 44.
};
let myApt = new Apartment(apt);
console.log(myApt);
// output will be original.
12

Back to enumerable.

let otherApt = {
floor: 12,
number: '12B',
size: 3400,
bedRooms: 3.4,
bathRooms: 2,
price: 400000,
amenities: {}
};

let keys = Object.keys(otherApt);
console.log(keys);
// Output
['floor', 'number', 'size', 'bedRooms',
'bathRooms', 'price', 'amenities']

In other words, we can iterate the properties of the object using for … in.
What if one of the properties, e.g. amenities, should not be available for iteration?
This can be done by setting {enumerable: false} in our descriptor

Object.defineProperty(otherApt, 'amenities', {enumerable: false});
const keys = Object.keys(otherApt);
console.log(keys);
// Output
[ 'floor', 'number', 'size', 'bedRooms', 'bathRooms', 'price']

The amenities property remains a part of the apt.prototype.

console.log(otherApt.hasOwnProperty(‘amenities’));
// Output
true

Takeaway

I hope this article will shed a little more light on what descriptor is and how you can use it.

If you like this article press 👏 clap button ♾️ times.
Feel free to ask a question.
Follow me on Twitter and Medium for blog updates.
Thanks a lot for reading!

--

--

Daniel Ostrovsky

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