DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Daniel
Daniel

Posted on • Originally published at Medium

Metaprogramming in JavaScript/TypeScript Part #1

Descriptor.

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: {...}
};
Enter fullscreen mode Exit fullscreen mode

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
//}
Enter fullscreen mode Exit fullscreen mode

Let’s take a closer look at the next:

value β€” this is the value assigned to the floor property at a certain moment

writable β€” defines whether it is possible to change the value

enumerable β€” defines if the floor property can or can’t be enumerated β€” (more on this later).

configurable β€” 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});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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});
Enter fullscreen mode Exit fullscreen mode

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.
};
Enter fullscreen mode Exit fullscreen mode

Let’s change the getFloor method:

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

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
Enter fullscreen mode Exit fullscreen mode

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'
]
Enter fullscreen mode Exit fullscreen mode

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' ]
Enter fullscreen mode Exit fullscreen mode

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

console.log(otherApt.hasOwnProperty('amenities'));
// Output
// true
Enter fullscreen mode Exit fullscreen mode

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 like button ♾️ times.
Feel free to ask a questions.
Follow me on Twitter and Medium and my website for blog updates.
Thanks a lot for reading!

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.