Metadata, in a nutshell, is extra information about the actual data. For example, if a variable represents an array, the length of that array is metadata. Similarly, each element in that array is data but the data-type of these elements is metadata. Loosely speaking, metadata is not the actual concern of a program, but it can help us achieve things quicker.
Let’s take a small example. If you need to design a function that prints information about other functions, what information would you print?
In the above example, the funcInfo
function takes a function as an argument and returns a string that contains function name and the number of arguments this function accepts. This information is contained in the function itself, however, we almost never use that in practice. Therefore, func.name
and func.length
can be considered as metadata.
In the Property Descriptors lesson, we learned about property descriptors of the object’s properties. A property descriptor is an object that configures how an object’s property behaves. For example, you can set an object’s property to be read-only (non-writable) or non-enumerable.
A property descriptor is like metadata of the object’s property. It doesn’t show up unless you look for it, perhaps using the Object.getPropertyDescriptor()
method or Reflect.getPropertyDescriptor()
method call. You can customize this metadata to change how the object and its properties behave and we learned that in the Property Descriptors lesson.
Metadata is what makes the metaprogramming possible. Metadata is necessary for reflection, especially for the introspection. For example, you can change the program behavior based on the number of arguments a function is designed to receive.
So now you can understand the power metadata has. It opens all kinds of interesting possibilities for metaprogramming. However, we are restricted by what JavaScript has to offer for metaprogramming which is not much. We have explored these features in the earlier lessons. Only if there was a feature that could let us add custom metadata to objects. That would solve almost all the problems 🥺.
Let’s welcome Reflect
’s metadata extension which does exactly that. There is a proposal to extends Reflect’s functionality to add custom metadata to objects and object properties.
Wait!!! I would not be so excited at this very moment because it is still a proposal and it hasn’t even been submitted to the ECMAScript. However, you can find a detailed proposal spec here and if you are looking for the reason why it wasn’t submitted to TC39, this GitHub issue thread may help you.
If you are a TypeScript developer and you have been working with Decorators, then you might have heard about the reflect-metadata
package. This package lets you add custom metadata to classes, class fields, etc. But in this lesson, we are not going to talk about TypeScript or decorators. Perhaps, we can cover them in separate lessons. In this lesson, we are going to look at what reflect-metadata
package offers and what we can do with it.
In the Reflect lesson, we learned that Reflect
API is a great tool to inspect objects and add metadata to change their behavior. For example, Reflect.has
method works like in
operator to check for the existence of a property on the object or its prototype chain. The Reflect.setPrototypeOf
method adds a custom prototype on the object, therefore changing its behavior. Reflect
provides many such methods that are used for reflection.
The Metadata Proposal (spec here), proposes some new methods to extends Reflect
’s capability. These methods, however, only meant to augment metaprogramming support of JavaScript. Let’s have a quick look.
// define metadata on a target object
Reflect.defineMetadata(metadataKey, metadataValue, target);
// define metadata on a target's property
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
The Reflect.defineMetadata
method lets you add a custom metadata value metadataValue
which could be any JavaScript value to the target
object (descendent of the Object
) or the target
object’s propertyKey
property. You can add as many metadata values as you want as each metadata value is identified by a metadataKey
.
// get metadata associated with target
let result = Reflect.getMetadata(metadataKey, target);
// get metadata associated with target's property
let result = Reflect.getMetadata(metadataKey, target, propertyKey);
Using the Reflect.getMetadata
method, you can extract the same metadata associated with the target
object or its properties with the metadataKey
.
In the previous lesson, we also learned about the internal slots and internal methods. These are the internal properties and methods of the object that holds the data and logic to operate on that data.
This metadata proposal proposes [[Metadata]]
internal slot for all ordinary objects. This internal slot could be either be null
which means the target
object doesn’t have any metadata or it could be a Map
element that holds the metadata with different keys for the target or its properties.
The Reflect.defineMetadata
calls the [[DefineMetadata]]
internal method and Reflect.getMetadata
uses [[GetMetadata]]
internal method to fetch it.
Let’s see this in practice. But before we do that, we need to install reflect-metadata
package. You can find the instructions to install it from this GitHub repository. You can install it using npm install reflect-metadata
command.
The require('reflect-metadata')
import statement is special. This imports the reflect-metadata
package which adds the proposed methods such as defineMetadata
to the Reflect
object at runtime and we can verify this by looking at the type of Reflect.defineMetadata
function. Therefore, this package is technically a polyfill.
Then we defined a target
object onto which we have added some metadata with the keys version
, info
and is
. The version
and info
was added directly on the target
while is
was added on the name
property. The metadata value is any possible JavaScript value.
The Reflect.getMetadata
returns the metadata associated with the target
or its property. If no metadata is found, it returns undefined
. Using these methods, you can associate any metadata with any object or its properties.
The target
is not mutated while registering metadata on itself or on its properties as you can see from the logs. In reality, this metadata will be stored in the [[Metadata]]
internal slot but this polyfill uses a WeakMap
to hold metadata for the target
.
You can check for the existence of a metadata value using hasMetadata
method and getMetadataKeys
returns the keys of the metadata values registered on a target
or its properties. If you want to get rid of a metadata value, then you can use the deleteMetadata
method.
// check for presence of a metadata key (returns a boolean)
let result = Reflect.hasMetadata(key, target);
let result = Reflect.hasMetadata(key, target, property);
// get all metadata keys (returns an Array)
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, property);
// delete metadata with a key (returns a boolean)
let result = Reflect.deleteMetadata(key, target);
let result = Reflect.deleteMetadata(key, target, property);
By default, getMetadata
, hasMetadata
, getMetadataKeys
also look up the prototype chain of the target
for the existence of metadata associated with the particular metadata key. Therefore, we also have getOwnMetadata
, hasOwnMetadata
and getOwnMetdatakeys
methods which basically do the same thing but they operate solely on the target
.
// get metadata value of an own metadata key
let result = Reflect.getOwnMetadata(key, target);
let result = Reflect.getOwnMetadata(key, target, property);
// check for presence of an own metadata key
let result = Reflect.hasOwnMetadata(key, target);
let result = Reflect.hasOwnMetadata(key, target, property);
// get all own metadata keys
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, property);
In the above example, proto
object is the prototype of the target
and therefore, all the metadata values defined on the proto
would be accessible on the target
. However, the *Own
methods do not search for a metadata key on the proto
. The result of the above program would be as follows.
This proposal also proposes Reflect.metadata
method but this not an ordinary Reflect
method as we have seen above. This method is a decorator factory which means when it is invoked with some arguments, it returns a decorator function that can be used to decorate a class or a class field.
We learned about JavaScript decorators in the “A minimal guide to JavaScript (ECMAScript) Decorators” lesson. JavaScript decorators are not a part of the ECMAScript standard at the moment and the proposal is still in stage 2. Therefore decorator proposal is still under active development.
@Reflect.metadata(metadataKey, metadataValue)
class MyClass {
@Reflect.metadata(metadataKey, metadataValue)
methodName() {
// ...
}
}
In the above snippet, the @Reflect.metadata
method call decorates the MyClass
class and methodName
method (property). Basically, it adds the metadataValue
metadata to these entities with metadataKey
key.
If you are wondering how this works, it’s actually pretty simple. The Reflect.metadata
method call returns a decorator function. This decorator function internally implements Reflect.defineMetadata
which adds metadata to the entity it is decorating such as the class or its property.
The problem here is that reflect-metadata
package (polyfill) implements the legacy version of the decorator proposal. TypeScript also implements the same version for decorators implementation. You can follow my article on decorators to understand the legacy pattern to design decorators.
Since we can’t implement the decorator pattern provided by this package in JavaScript natively, I would need to demonstrate it using TypeScript. But you can ignore the types in this program so that it looks syntactically similar to a JavaScript program.
💡 You can also use this babel plugin to transpile JavaScript with legacy decorators to vanilla JavaScript code that works natively.
As you can see in the above example, the @Reflect.metadata
was successfully able to add metadata on the Person
class which we later simply extracted using Reflect.getMetadata(<key>, Person)
method. You can also create your own decorator factory such as myDecorator
which returns the decorator returned by the Reflect.metadata
call.
💡 We will learn more about the decorator pattern in TypeScript and the use of
reflect-metadata
package in a separate TypeScript lesson.
That’s pretty much it for the reflect-metadata
package and metadata proposal. The use-cases of this proposal are endless. One would say this is the holy grail of the metaprogramming in JavaScript. We just have to wait and see when this becomes part of the ECMAScript proposal.
In the Proxy lesson, we learned that every proxy handler
trap has an equivalent Reflect
static method. You would have hoped that the reflect-metadata
package also provides support for Proxy
but it does not at the moment. You can track this feature from this GitHub issue.
🙋 Let me know in the comments what do you feel about this proposal.