Introduction to reflect-metadata package and its ECMAScript proposal

In this lesson, we are going to take a look at the reflect-metadata package used by TypeScript to design decorators. This package is primarily used as a polyfill for the Reflect API's "metadata-extension" ECMAScript proposal.

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.

#typescript #reflect-metadata