Metaprogramming In Javascript

Metaprogramming In Javascript

ยท

4 min read

Metaprogramming in javascript gives you the ability to customize your object's fundamental behavior, such as how your object would behave if you try to set its properties, convert it into a string or number, and much more. ๐Ÿง™๐Ÿผโ€โ™‚๏ธ

toPrimitive, toString and valueOf

You can override how your object behaives when it needs to be converted into primative types, for example if you are trying turn it into string or into number for some operations.

const object1 = {
  [Symbol.toPrimitive](hint) {
    if (hint == 'number') {
      return 42;
    }

    if (hint == 'string') {
      return 'I am a string';
    }

    if(hint == 'default') {
      return 'I am the default';
    }
  },
  name: 'Mark'
};

console.log(+object1 == 42); // hint number
// true
console.log(`${object1}` == 'I am a string'); // hint string
// true
console.log(object1 == 'I am the default'); // no hint, default
// true

(Full list of symbol methods you can override on MDN)

You can also do it globally for all objects

const object2 = { name: 'John' };

Object.prototype.valueOf = () => 42;
Object.prototype.toString = () => 'I am a string';

console.log(object2);
// { name: 'John' }
console.log(`${object2}`);
// I am a string
console.log(+object2);
// 42

A useful example can be that instead of getting [object Object] you can turn all objects into JSON strings when converted to a string

const object2 = { name: 'John' };

console.log(`${object2}`);
// [object Object]

Object.prototype.toString = function() { return JSON.stringify(this); };
console.log(`${object2}`);
// {"name":"John"}

Defining object's properties with style

Object.defineProperty() is a method that you can use to not only define property of your object but also define certain settings/descriptors for your property.

const product = {};
Object.defineProperty(product, 'price', {value: 42});
console.log(product.price);
// 42

Descriptor writable(default false) if false property can't be reassing with assigning operator =

const product = {};
Object.defineProperty(product, 'price', {
  value: 42,
  writable: false
});
product.price = 0;
console.log(product.price);
// 42

Descriptor enumerable(default false) if false property is hidden while enumeration

const product = {
  name: 'Bicycle',
  category: 'Transportation'
};
Object.defineProperty(product, 'price', {
  value: 42,
  enumerable: false
});
console.log(product);
// { name: 'Bicycle', category: 'Transportation' }
for (const property in product) {
  console.log(`${property}: ${product[property]}`);
}
// name: Bicycle
// category: Transportation

Descriptor configurable(default false) if false property can't be deleted and if descriptors, beside value and writable, can be changed.

const product = {
  name: 'Bicycle',
  category: 'Transportation'
};

Object.defineProperty(product, 'price', {
  value: 42,
  enumerable: true,
  configurable: false,
});

delete product.price;
console.log(product);
// { name: 'Bicycle', category: 'Transportation', price: 42 }

Object.defineProperty(product, 'price', {
  enumerable: false
});
// TypeError: Cannot redefine property

Descriptor get and set are method that are getting invoket when user tries to get or set value of the property.

const product = {};

Object.defineProperty(product, 'price', {
  get() { return 100; },
  set(newValue) { console.log(newValue) },
});

console.log(product.price);
// 100
product.price = 'I will get console logged';
// I will get console logged
console.log(product.price);
// 100

Proxy

A Proxy is an object that lets you add intercepts/overrides for your object's fundamental methods. Proxy takes two arguments; the first is the target object you will expand with your intercepts, the second is the object that contains your intercepts. With power of proxies you can: customize getter behavior for your object and its properties

const person = { name: 'Mark', profession: 'Web Developer' };
const handler = {
  get: function(target, prop, receiver) {
    if(prop === 'name') {
      return "Hello, I'm " + target[prop];
    }
    return target[prop];
  }
};
const proxied = new Proxy(person, handler);
console.log(proxied.name);
// Hello, I'm Mark
console.log(proxied.profession);
// Web Developer

Also customize setters behavior, maybe add some validation to accept only certain values for some properties

const barCard = { name: 'John' };
const handler = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (value < 18) {
        throw  new Error('Minimal drinking age is 18!');
      }
    }

    obj[prop] = value;
    return true;
  }
};
const proxied = new Proxy(barCard, handler);
proxied.age = 16;
// Minimal drinking age is 18!

You can also intercept deleteProperty to protect certain properties from deletion

const product = { sku: '12345', name: 'Bicycle', price: 42 };
const handler = {
  deleteProperty: function (target, prop) {
    if (prop in target && prop !== 'sku') {
      delete target[prop];
    }
    return false;
  },
};
const proxied = new Proxy(product, handler);
delete proxied.sku;
console.log(proxied);
// { sku: '12345', name: 'Bicycle', price: 42 }

A complete list of proxyable methods is available on MDN

ย