TypeScript’s Reflect Metadata: What it is and How to Use it

Jazim Abbas
Bits and Pieces
Published in
7 min readFeb 16, 2023

--

Table of Contents

What is Reflect API

The Reflect API in TypeScript provides a means to interact with metadata associated with decorators. Decorators are features used to add additional functionality to classes, properties, and methods. The Reflect API includes methods for accessing and modifying metadata for a given decorator, as well as methods for examining decorators on a class or its members. The Reflect.metadata method can be used to define metadata on a decorator.

To enable this feature, the following must be set in the tsconfig.json file:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

The “experimentalDecorators” setting turns on decorator support, while the “emitDecoratorMetadata” setting emits data required by the reflect-metadata package.

Problems

As JavaScript applications grow in size, tools such as inversion of control containers and run-time type assertions are becoming necessary to manage the increasing complexity. However, the lack of reflection in JavaScript limits the implementation of these tools and features, compared to programming languages like C# or Java.

A comprehensive reflection API would enable us to dynamically examine an object at run-time and retrieve information such as the entity’s name, type, implemented interfaces, property names and types, and constructor argument names and types.

Currently, JavaScript offers functions like Object.getOwnPropertyDescriptor() and Object.keys() to retrieve limited information about an object, but a full-fledged reflection API is needed for advanced development tools.

Reflect-Metadata Package

The Reflect-Metadata package empowers us to perform advanced tasks in decorators by collecting metadata about classes, properties, methods, and parameters.

There are three available reflect metadata design keys:

  • Type metadata uses the “design:type” metadata key.
  • Parameter type metadata uses the “design:paramtypes” metadata key.
  • Return type metadata uses the “design:returntype” metadata key.

Examples

import "reflect-metadata";

// Define a decorator
function MyDecorator(metadata: string) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata("my-decorator", metadata, target, propertyKey);
};
}

class MyClass {
@MyDecorator("some metadata")
myMethod() {}
}

// Retrieve the metadata for the decorator
const metadata = Reflect.getMetadata("my-decorator", MyClass.prototype, "myMethod");
console.log(metadata); // "some metadata"

In this example, a decorator called MyDecorator is defined. It takes a single argument, “metadata”, which is used to store metadata for the decorated property or method. The decorator utilizes the Reflect.defineMetadata method to set the metadata for the decorated property or method. Afterwards, the Reflect.getMetadata method can be used to retrieve the metadata set by the decorator. Additionally, the Reflect.getOwnMetadata and Reflect.getMetadataKeys methods can be used to obtain metadata from the decorator and to retrieve the metadata keys, respectively.

Realistic Scenario

import "reflect-metadata";

// Define a decorator to add a role to a user
function Role(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// check for permission here
if (!checkRole(role)) {
throw new Error("Access Denied");
}

Reflect.defineMetadata("role", role, target, propertyKey);
return originalMethod.apply(this, args);
};
};
}

class User {
private _username: string;
private _password: string;

constructor(username: string, password: string) {
this._username = username;
this._password = password;
}

@Role("admin")
getUsername() {
return this._username;
}

@Role("admin")
setPassword(password: string) {
this._password = password;
}

@Role("user")
changePassword(oldPassword: string, newPassword: string) {
if (this._password === oldPassword) {
this._password = newPassword;
} else {
throw new Error("Invalid password");
}
}
}

const user = new User("JohnDoe", "password123");

const role = Reflect.getMetadata("role", user, "getUsername");
console.log(role); // "admin"

const role2 = Reflect.getMetadata("role", user, "changePassword");
console.log(role2); // "user"

In this example, the Role decorator is added to three methods in the User class, each with a different role of either “admin” or “user”. Later, the Reflect.getMetadata method can be used to retrieve the role set by the decorator for each method. For instance, the getUsername and setPassword methods have a role of “admin”, while the changePassword method has a role of “user”. This allows you to use the role metadata to verify the user’s permissions within the application.

It’s important to keep in mind that this is merely an example, and a more secure method for storing and verifying user credentials and roles should be used in real-world scenarios.

In Dependency injection

Dependency injection is a method of supplying objects with the dependencies they need to operate correctly, often by passing them through the object’s constructor or by utilizing a framework that supports dependency injection. In TypeScript, decorators can be employed to supply additional information about a class’s dependencies, which can then be utilized by a dependency injection framework to automatically instantiate and supply the necessary dependencies.

If you have previously used Angular or NestJS, then you are likely aware that both of these frameworks heavily rely on dependency injection, particularly with the use of decorators and the reflect-metadata package. You have likely encountered the “@Injector()” decorator frequently in these frameworks.

Example

Here’s an example of using a “@Injectable” decorator to supply metadata about a class’s dependencies:

import "reflect-metadata";

function Injectable() {
return function (target: any) {
Reflect.defineMetadata("injectable", true, target);
};
}

@Injectable()
class MyService {
constructor(private _dependency: MyDependency) {}

doSomething() {
this._dependency.doSomething();
}
}

class MyDependency {
doSomething() {
console.log("MyDependency is doing something");
}
}

In this example, the “MyService” class has a dependency on an instance of “MyDependency”. A “@Injectable” decorator has been defined and applied to the “MyService” class. The decorator uses the “Reflect.defineMetadata” method to store metadata on the class, in this case, a boolean value of “true.

A dependency injection framework could then use this metadata to automatically instantiate and provide the necessary dependency, MyDependency, for an instance of MyService.

Here’s a simple example of how a DependencyInjection class can use the metadata to instantiate the dependencies.

class DependencyInjection {
static get<T>(target: any): T {
const isInjectable = Reflect.getMetadata("injectable", target);
if (!isInjectable) {
throw new Error("Target is not injectable");
}

const dependencies = Reflect.getMetadata("design:paramtypes", target) || [];
const instances = dependencies.map((dep) => DependencyInjection.get(dep));
return new target(...instances);
}
}

const myService = DependencyInjection.get(MyService);
myService.doSomething(); // "MyDependency is doing something"

In this example, the DependencyInjection class contains a static method called “get”, which takes a target class as an input. The method checks if the class has the @Injectable metadata. If it is injectable, the method will then use Reflect.getMetadata(“design:paramtypes”, target) to retrieve the constructor dependencies of the class and recursively calls DependencyInjection.get(dep) to instantiate these dependencies.

It’s important to note that this is just a simple example and that, in most cases, you would use an existing dependency injection framework like Inversify or NestJS rather than implementing your own.

Why do we need it?

  1. A common application of decorators and metadata is for implementing metadata-based security or access control. By using decorators to specify roles or permissions for different elements of the application and using the Reflect API to retrieve that metadata, you can easily verify a user’s permissions before granting them access to perform specific actions.
  2. Another use case for using decorators and metadata is to add metadata-based validation. By using decorators to define validation rules for properties or methods, and using the Reflect API to retrieve and apply those validation rules, you can ensure data integrity and consistency in your application. e.g. if you’ve used the class-validator package, then you know how they are heavily rely on Reflect API.
  3. Decorators and the Reflect API can also be utilized to improve code organization and maintainability by providing a modular and reusable method of adding functionality to classes, properties, and methods.
  4. It also provides a way to add metadata to properties or methods, which can improve the code’s readability by providing additional context and information.
  5. and many more …

Summary

In summary, the Reflect API provides a powerful and flexible way to work with metadata in TypeScript, offering the ability to enhance classes, properties, and methods through the use of metadata in various ways, such as security, validation, and code organization.

If you have any questions or concerns regarding the information provided above, or if you would like me to cover a specific topic in the future, please let me know in the comments section.

If you want to support me, buy me a coffee here. Thanks for your time and support. Happy Coding.

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--