Using the Reflection and Reflection Metadata APIs with TypeScript Decorators

; Date: Sun Feb 20 2022

Tags: TypeScript

Making full use of TypeScript decorators requires understanding the Reflection and Reflection Metadata API's. The latter lets you store data in object metadata so that decorators can work together to create complex behaviors.

Reflection, in software development, is the ability for the code to look at itself, to examine or introspect its data structures, and to modify its own structure and behavior, while it is executing. But, that was a lot of big words, and not enough grounding in practical understanding.

This article seeks to be a sensible grounded introduction to using Reflection and the Reflection Metadata API, in TypeScript, alongside TypeScript Decorators. In other words, we'll get our hands in some code so we can understand what those big words meant. A lot of what's here is also useful in straight JavaScript.

The JavaScript Reflection API has been around for a few years, first appearing in Node.js in v6.0. Most of us probably don't have to use it, and maybe don't know about these API's. But Reflection, and the Reflect Metadata extension, is very useful with implementing TypeScript decorators, among other uses.

The core Reflection API focuses on examining data about properties and other aspects of JavaScript objects. The Mozilla documentation describes it as providing methods for interceptable JavaScript operation, which is more big words that don't really help with our understanding.

The methods of the Reflection API are attached to the Reflect object. That object is simply there, and you don't do anything special to make it exist. It does not have a constructor, it simply exists, and it contains static functions (methods) which can be used in code to do things related to the big words shown above. Namely, the core Reflection API allows code to retrieve data about objects, and manipulate that data or object structure.

The Reflection Metadata API is a proposed addition to the Reflection API adding additional metadata functionality. It is meant to be closely tied to the decorators proposal, and is expected to be standardized after decorators are standardized.

Using this API requires a teensy bit of setup. You first install the reflect-metadata package, and second you put import 'reflect-metadata' in your code. The package contains a polyfill which adds the Metadata API to the Reflect object.

This article is part of a series:

Setting up Reflection Metadata API for TypeScript

To enable these API's in TypeScript, along with Decorators, see the instructions in Deep introduction to using and implementing TypeScript decorators

Enabling the Reflection Metadata API requires installing the reflect-metadata package as so:

$ npm install reflect-metadata --save

Afterward, add this to your code:

import "reflect-metadata";

In your tsconfig.json file make these settings:

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

The first, experimentalDecorators, turns on decorator support. The second, emitDecoratorMetadata, emits data required by the reflect-metadata package. This package enables us to do powerful things in decorators by recording metadata about classes, properties, methods, and parameters.

Using Reflection API to retrieve data about JavaScript objects

Let's get started by using the Reflection API to retrieve data about objects.

The first method to use, Reflect.has, lets you check to see if an object has a property of a given name. To see what that means, start with this class:

class HasExample {

    year: number;
    #privateYear: number;
    #title: string;

    set title(nt: string) { this.#title = nt; }
    get title() { return this.#title; }

    // set privateYear(ny: number) { this.#privateYear = ny; }
    // get privateYear() { return this.#privateYear; }

    area(x: number, y: number) {
        return x * y;
    }

    constructor(privateYear: number) {
        this.#privateYear = privateYear;
    }
}

This class definition has a few things against which we can run queries. Private fields like #title has certain access restrictions, for example. To test it, add this code:

const hs = new HasExample(2222);

console.log(`year ${Reflect.has(hs, 'year')}`);
hs.year = 2022;
console.log(`year ${Reflect.has(hs, 'year')}`);

console.log(`privateYear ${Reflect.has(hs, 'privateYear')}`);
console.log(`title ${Reflect.has(hs, 'title')}`);
console.log(`area ${Reflect.has(hs, 'area')}`);
console.log(`xyzzy ${Reflect.has(hs, 'xyzzy')}`);

Running this we get the following output:

$ npx ts-node lib/reflection/has.ts
year false
year true
privateYear false
title true
area true
xyzzy false

The property year starts out not existing, then once we set a value it exists. We saw this behavior when exploring property decorators, where we saw Object.hasOwnProperty return false until we assigned a value to the property.

For #privateYear, even though it has a value assigned through the constructor, is a private property that we simply cannot access. Therefore, it's no surprise that Reflect.has returns false for this field.

For #title the story would be the same, but we created accessor functions for that property. When Reflect.has says this property exists, it is referring to the accessors. Likewise, area isn't technically a property, but it is a method. And, xyzzy simply doesn't exist, so we get false.

Next there is Reflect.ownKeys, to get the property names, Reflect.getOwnPropertyDescriptor to retrieve the PropertyDescriptor, and Reflect.getPrototypeOf to retrieve the object prototype. Each parallels methods in Object to get the same data. Add this code to the script:

console.log({
    ownKeys: Reflect.ownKeys(hs),
    keys: Object.keys(hs)
});

console.log({
    ownProperty: Reflect.getOwnPropertyDescriptor(hs, "year"),
    objectProperty: Object.getOwnPropertyDescriptor(hs, "year")
});

console.log({
    ownProperty: Reflect.getOwnPropertyDescriptor(hs, "title"),
    objectProperty: Object.getOwnPropertyDescriptor(hs, "title")
});

console.log({
    reflectPrototype: Reflect.getPrototypeOf(hs),
    objectPrototype: Object.getPrototypeOf(hs),
    prototype: HasExample.prototype
});

This shows the equivalence between certain Reflect and Object methods. When run, we get this output:

{ ownKeys: [ 'year' ], keys: [ 'year' ] }
{
  ownProperty: { value: 2022, writable: true, enumerable: true, configurable: true },
  objectProperty: { value: 2022, writable: true, enumerable: true, configurable: true }
}
{ ownProperty: undefined, objectProperty: undefined }
{ reflectPrototype: {}, objectPrototype: {}, prototype: {} }

We're getting the same data through each avenue.

Defining, Getting, Setting, and Deleting properties (CRUD) using Reflection

The CRUD principle is usually applied to applications using a database. But, using Reflection methods, we can create a property, read the value of a property, update the value of a property, and delete a property. In other words, we can do the CRUD operations on JavaScript objects using Reflection.

Let's start with this simple object, and call some methods to read its initial state:

const example = {
    prop1: "property1",
    prop2: 42
};

console.log({
    ownKeys: Reflect.ownKeys(example),
    keys: Object.keys(example)
});

console.log('prop1', Reflect.get(example, 'prop1'));
console.log('prop2', Reflect.get(example, 'prop2'));
console.log(example);

We've created an anonymous object, and used several methods to read the keys of properties in the object, and to read their values. The Reflect.get method is equivalent to using the get accessor.

The output is:

{ ownKeys: [ 'prop1', 'prop2' ], keys: [ 'prop1', 'prop2' ] }
prop1 property1
prop2 42
{ prop1: 'property1', prop2: 42 }

We clearly have prop1 and prop2, and the values are as expected. This demonstrates several ways to Read data from an object using Reflecton.

To Create a property, run this method:

Reflect.defineProperty(example, 'prop3', {
    value: "Property #3",
    enumerable: true,
    writable: true,
    configurable: true
});

This is similar to Object.defineProperty, and defines a property on the object, with a given name, with the given PropertyDescriptor. In this case the property is named prop3, with the value shown in the descriptor. The settings for writable, enumerable, and configurable control its behavior.

console.log({
    ownKeys: Reflect.ownKeys(example),
    keys: Object.keys(example)
});

console.log('prop3', Reflect.get(example, 'prop3'));
console.log(example);

console.log(Object.getOwnPropertyDescriptors(example));

Let's again Read the values and verify that indeed the property was created:

{
  ownKeys: [ 'prop1', 'prop2', 'prop3' ],
  keys: [ 'prop1', 'prop2', 'prop3' ]
}
prop3 Property #3
{ prop1: 'property1', prop2: 42, prop3: 'Property #3' }
{
  prop1: {
    value: 'property1',
    writable: true,
    enumerable: true,
    configurable: true
  },
  prop2: { value: 42, writable: true, enumerable: true, configurable: true },
  prop3: {
    value: 'Property #3',
    writable: true,
    enumerable: true,
    configurable: true
  }
}

It is indeed there, showing up in the property keys, and the property descriptors, and with the expected value. If enumerable is false, it will not show up in the property keys, and will not show up in the simple object dump.

To Update the property, we can use normal JavaScript:

example['prop3'] = 'NEW AND IMPROVED Property #3';

console.log('prop3', Reflect.get(example, 'prop3'));
console.log(example);

Reflect.set(example, 'prop3', 'ULTIMATELY IMPROVED Property #3');

console.log('prop3', Reflect.get(example, 'prop3'));
console.log(example);

Why did we use example['prop3'] rather than example.prop3? The TypeScript compiler executes at compile time, and Reflect.defineProperty executes at runtime, and therefore the compiler cannot know that prop3 will exist, and therefore the compiler will give a compilation error.

Using example['prop3'] works great in this case. We can either use a normal JavaScript assignment to update the property, or use Reflect.set to update the property.

The output is as so:

prop3 NEW AND IMPROVED Property #3
{
  prop1: 'property1',
  prop2: 42,
  prop3: 'NEW AND IMPROVED Property #3'
}
prop3 ULTIMATELY IMPROVED Property #3
{
  prop1: 'property1',
  prop2: 42,
  prop3: 'ULTIMATELY IMPROVED Property #3'
}

Here we show that the property is updated this way. If you set writable to false, the property will not be changed.

The last thing is to demonstrate deleting properties.

// console.log(Reflect.deleteProperty(example, 'prop3'));
delete example['prop3'];
// console.log(Reflect.deleteProperty(example, 'prop1'));
delete example.prop1;

console.log({
    ownKeys: Reflect.ownKeys(example),
    keys: Object.keys(example)
});
console.log(example);

console.log(Object.getOwnPropertyDescriptors(example));

The normal JavaScript way to delete a property is with the delete statement. But, we can use Reflect.deleteProperty as well. In this case we've deleted not just prop3 but prop1, which should only leave prop2.

Because Reflect.deleteProperty returns a Boolean indicating whether the deletion worked, or not. The value for the configurable attribute of the property determines whether it was deletable.

{ ownKeys: [ 'prop2' ], keys: [ 'prop2' ] }
{ prop2: 42 }
{
  prop2: { value: 42, writable: true, enumerable: true, configurable: true }
}

Indeed, that's the only remaining property.

We're able to use either normal JavaScript code, or Reflection, to create, read, update, or delete properties on a JavaScript object.

Using Reflection Metadata to store/read/update/delete additional data about TypeScript objects

Metadata in this case means the data describing the structure or data types of JavaScript objects. Using this API we can also store data as metadata, which is kept completely separate from the actual data stored by the object. We'll use metadata along with a couple decorators later for a very interesting capability.

There are enough Reflection Metadata methods to also implement the CRUD operations. Let's start here:

import 'reflect-metadata';

class MetadataExample {
    prop1: string = 'prop1';
    prop2: number = 42;
}

const example2 = new MetadataExample();

console.log({
    ownMetadataKeys: Reflect.getOwnMetadataKeys(example2),
    metadataKeys: Reflect.getMetadataKeys(example2)
});

This creates a simple class, creates an instance of the class, then reads out some data about the class. It is important to remember to import reflect-metadata. Failing to do so results in this error:

error TS2339: Property 'getOwnMetadataKeys' does not exist on type 'typeof Reflect'.

Once you make sure to import that package, the output is like so:

$ npx ts-node lib/reflection/metadata.ts 
{ ownMetadataKeys: [], metadataKeys: [] }

In other words, there are no Metadata Keys yet. But, what are they?

Reflect.defineMetadata('metaProp1', 'prop1', example2);

console.log({
    ownMetadataKeys: Reflect.getOwnMetadataKeys(example2),
    metadataKeys: Reflect.getMetadataKeys(example2)
});

What this has done is to Create a metadata property, with the Key of metaProp1, the Value of prop1 on the example2 object.

In other words, the metadata key is the name for the metadata property. This is like a regular object key is the name of a regular JavaScript object property.

We can also define metadata on a property of an object:

Reflect.defineMetadata('metaKey1ForProp1', 'data stored in prop1',
        example2, 'prop1');

console.log({
    prop1OwnMetadataKeys: Reflect.getOwnMetadataKeys(example2, 'prop1'),
    prop1MetadataKeys: Reflect.getMetadataKeys(example2, 'prop1')
});

Here we have defined metaKey1ForProp1 on example2.prop1, and are retrieving the metadata keys for that property.

{
  prop1OwnMetadataKeys: [ 'metaKey1ForProp1' ],
  prop1MetadataKeys: [ 'metaKey1ForProp1' ]
}

Another function lets us determine whether metadata properties exist:

console.log({
    hasMetadata: Reflect.hasMetadata('metaProp1', example2),
    hasOwnMetadata: Reflect.hasOwnMetadata('metaProp1', example2),
    prop1NotHasMetadata: Reflect.hasMetadata('metaProp1', example2, 'prop1'),
    prop1NotHasOwnMetadata: Reflect.hasOwnMetadata('metaProp1', example2, 'prop1'),
    prop1HasMetadata: Reflect.hasMetadata('metaKey1ForProp1', example2, 'prop1'),
    prop1HasOwnMetadata: Reflect.hasOwnMetadata('metaKey1ForProp1', example2, 'prop1'),
});

This shows us using hasMetadata and hasOwnMetadata to query for metadata properties on both example2 and example2.prop1.

{
  hasMetadata: true,
  hasOwnMetadata: true,
  prop1NotHasMetadata: false,
  prop1NotHasOwnMetadata: false,
  prop1HasMetadata: true,
  prop1HasOwnMetadata: true
}

The two which show false are because we used the wrong metadata key value.

We can Read the metadata values like so:

console.log({
    metadata: Reflect.getMetadata('metaProp1', example2),
    ownMetadata: Reflect.getOwnMetadata('metaProp1', example2),
    prop1NotMetadata: Reflect.getMetadata('metaProp1', example2, 'prop1'),
    prop1NotOwnMetadata: Reflect.getOwnMetadata('metaProp1', example2, 'prop1'),
    prop1Metadata: Reflect.getMetadata('metaKey1ForProp1', example2, 'prop1'),
    prop1OwnMetadata: Reflect.getOwnMetadata('metaKey1ForProp1', example2, 'prop1'),
});

With getMetadata and getOwnMetadata we read the current value of a metadata property.

{
  metadata: 'prop1',
  ownMetadata: 'prop1',
  prop1NotMetadata: undefined,
  prop1NotOwnMetadata: undefined,
  prop1Metadata: 'data stored in prop1',
  prop1OwnMetadata: 'data stored in prop1'
}

And, this returned the expected values. For the middle two, we again passed an incorrect metadata key, and was given undefined as the result.

To update a metadata value, we call defineMetadata again:

Reflect.defineMetadata('metaProp1', 'NEW IMPROVED prop1', example2);
Reflect.defineMetadata('metaKey1ForProp1', 'NEW IMPROVED prop1 on prop1', example2, 'prop1');

And, rerunning the last example we get this output:

{
  metadata: 'NEW IMPROVED prop1',
  ownMetadata: 'NEW IMPROVED prop1',
  prop1NotMetadata: undefined,
  prop1NotOwnMetadata: undefined,
  prop1Metadata: 'NEW IMPROVED prop1 on prop1',
  prop1OwnMetadata: 'NEW IMPROVED prop1 on prop1'
}

Finally, we can Delete metadata keys using deleteMetadata:

Reflect.deleteMetadata('metaProp1', example2);
Reflect.deleteMetadata('metaKey1ForProp1', example2, 'prop1');

Rerunning the example we get this output:

{
  metadata: undefined,
  ownMetadata: undefined,
  prop1NotMetadata: undefined,
  prop1NotOwnMetadata: undefined,
  prop1Metadata: undefined,
  prop1OwnMetadata: undefined
}

The metadata values are now gone.

Using the Reflect.metadata decorator on a TypeScript class

The reflect-metadata package also supplies a decorator we can use to add metadata values to things. It is used as follows:

import 'reflect-metadata';

@Reflect.metadata('decoratedClass', 'value')
class MetadataDecoratorExample {
  // apply metadata via a decorator to a method (property)
  @Reflect.metadata('decoratedMethod', 'method value')
  method(param1?: number, param2?: string): string {
      return 'Hello, World!';
  }
}

const mde = new MetadataDecoratorExample();

This simply sets a metadata value. To query the metadata values, we can add this:

console.log({
    classClassKeys: Reflect.getMetadataKeys(MetadataDecoratorExample),
    classClassMetadata: Reflect.getMetadata('decoratedClass', MetadataDecoratorExample),
    classKeys: Reflect.getMetadataKeys(mde),
    methodKeys: Reflect.getMetadataKeys(mde, 'method'),
    methodReturn: Reflect.getMetadata('design:returntype', mde, 'method'),
    methodParams: Reflect.getMetadata('design:paramtypes', mde, 'method'),
    methodType: Reflect.getMetadata('design:type', mde, 'method'),
    methodDecorated: Reflect.getMetadata('decoratedMethod', mde, 'method'),
});

The output for this is:

{
  classClassKeys: [ 'decoratedClass' ],
  classClassMetadata: 'value',
  classKeys: [],
  methodKeys: [
    'design:returntype',
    'design:paramtypes',
    'design:type',
    'decoratedMethod'
  ],
  methodReturn: [Function: String],
  methodParams: [ [Function: Number], [Function: String] ],
  methodType: [Function: Function],
  methodDecorated: 'method value'
}

The first two lines are from the decorator attached to the class. We see that the metadata key shows up here, as well as the value, but not if we make the same query on the class instance.

Starting with methodKeys we make queries against the instance method, named method. We found the expected metadata key, decoratedMethod, as well as these other three. We understand that those three metdata keys are automatically supplied by TypeScript in certain circumstances. For the decoratedMethod key we received the expected value.

For the three design keys, going by the outputs, and by the property names, the purpose seems to be:

  • design:returntype - Documents the return type of the property
  • design:paramtypes - Documents the parameter types of the property
  • design:type - Documents the type of thing is the property

Using Reflection Metdata in TypeScript decorator implementation

Since the Reflection Metadata API is meant to be used with decorator implementation, let's explore how that can work. In our article on method decorators, we developed an example which used an array to store data from parameter decorators for use in a method decorator. By itself a parameter decorator cannot do much other than record some data. A method decorator can, however, intercept the method call and, for example, modify the arguments, which is what that example did.

Let's rewrite that example to use reflection metadata instead.

class DefaultExample {

    @SetDefaults
    volume(
        z: number,
        @ParamDefault<number>(10) x?: number,
        @ParamDefault<number>(15) y?: number,
        title?: string
    ) {
        const ret = {
            x, y, z, volume: x * y * z, title
        };
        console.log(`volume `, ret);
        return ret;
    }
}

This is the class definition from that example. It uses a decorator, ParamDefault, to define a default value for optional parameters where no value was supplied. The SetDefaults decorator installs an overriding function on this method, which uses data coming from ParamDefault decorators to supply default values for any parameters that were not supplied by the caller.

import 'reflect-metadata';

const DEFAULTS = 'defaults';

We import the reflect-metadata package. Then, we define DEFAULTS to contain a string we'll use for the metadata key in which to store default values. This technique is similar to what we might do in C programming, where we use #define to create symbolic constants. Using this for metadata keys lets us be sure of consistently using the same key string.

function ParamDefault<T>(value: T) {
    return (target: Object, propertyKey: string | symbol,
        parameterIndex: number)=> {

        const defaults = Reflect.getMetadata(DEFAULTS, target, propertyKey)
                        || {};
        defaults[parameterIndex] = value;
        Reflect.defineMetadata(DEFAULTS, defaults, target, propertyKey);
    }
}

This is the rewritten ParamDefault decorator. It first gets the existing metadata value, if any. We know from the above that if no value currently exists, undefined will be returned, in which case we will substitute an empty object.

The result of this is that the DEFAULTS metadata value contains what we can call a sparse array holding the default values. Only the indexes where a ParamDefault decorator is used will have a default value.

We then use Reflect.defineMetadata to update the DEFAULTS.

function SetDefaults(target: Object, propertyKey: string,
    descriptor: PropertyDescriptor) {

    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`SetDefaults before ${propertyKey}`, args);

        const defaults = Reflect.getMetadata(DEFAULTS, target, propertyKey)
                        || {};
        for (const key of Object.keys(defaults)) {
            let def = defaults[key];
            if (typeof args[key] === 'undefined'
                || args[key] === null) {
                args[key] = def;
            }
        }

        console.log(`SetDefaults after substitution ${propertyKey}`, args);
        const result = originalMethod.apply(this, args);
        console.log(`SetDefaults after ${propertyKey}`, result);
        return result;
    }
}

Here we have the SetDefaults decorator. Read the article on method decorators to get a complete description of what's happening. The bottom line is that originalMethod is the actual function. We have defined a replacement function to override that function. The override function looks for any default values which should be supplied, changes the arguments array, then calls the original function.

The change is that we use Reflect.getMetadata to retrieve the data about default values. The array returned by Object.keys(defaults) tells us which parameter indexes have default values. If the corresponding args array entry is either undefined or null, we know no value was supplied, and we can instead set the default value from the decorator.

To test this, we have the following:

const de = new DefaultExample();

console.log(de.volume(10));
console.log('----------------------');
console.log(de.volume(20, null, 20, "Second"));
console.log('----------------------');
console.log(de.volume(30, 30, null));
console.log('----------------------');
console.log(de.volume(40, 40, 50, "Fourth"));

This tries every combination of default values in our application. The output we get is:

SetDefaults before volume [ 10 ]
SetDefaults after substitution volume [ 10, 10, 15 ]
volume  { x: 10, y: 15, z: 10, volume: 1500, title: undefined }
SetDefaults after volume { x: 10, y: 15, z: 10, volume: 1500, title: undefined }
{ x: 10, y: 15, z: 10, volume: 1500, title: undefined }
----------------------
SetDefaults before volume [ 20, null, 20, 'Second' ]
SetDefaults after substitution volume [ 20, 10, 20, 'Second' ]
volume  { x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
SetDefaults after volume { x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
{ x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
----------------------
SetDefaults before volume [ 30, 30, null ]
SetDefaults after substitution volume [ 30, 30, 15 ]
volume  { x: 30, y: 15, z: 30, volume: 13500, title: undefined }
SetDefaults after volume { x: 30, y: 15, z: 30, volume: 13500, title: undefined }
{ x: 30, y: 15, z: 30, volume: 13500, title: undefined }
----------------------
SetDefaults before volume [ 40, 40, 50, 'Fourth' ]
SetDefaults after substitution volume [ 40, 40, 50, 'Fourth' ]
volume  { x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }
SetDefaults after volume { x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }
{ x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }

As you can see, the correct values were substituted in every case.

Using Reflection Metadata simplified this example a fair amount. Using an external array like we'd done is not a clean solution.

Summary

TypeScript has a feature for default parameter values. Therefore, it isn't a great big earth-shattering advancement that we can build decorators that for injecting default values. What is the earth-shattering thing is the capability with a few lines of code to implement a feature of this consequence. Many languages do not have a default parameter value feature, and also do not have any capability of retrofitting such a feature. Using a pair of TypeScript decorators, and about 50 lines of code, we were able to implement a credible useful feature.

In the next article we'll take this further, and demonstrate develop automatic runtime data validation using TypeScript decorators.

About the Author(s)

(davidherron.com) David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.

Books by David Herron

(Sponsored)