Deep introduction to parameter decorators in TypeScript

; Date: Tue Feb 22 2022

Tags: TypeScript

Decorators allow us to add additional information to classes or methods in TypeScript, and are similar to annotations such as in Java. Parameter decorators are applied to method parameter definitions in TypeScript. With them, we can record information about parameters, including customized information, using that data in other features.

In this article we will explore developing decorators for method parameters in TypeScript. This is what parameter decorators look like in practice:

@ClassDecorator()
class A {
...
    @MethodDecorator()
    fly(
        @ParameterDecorator(?? optional parameters)
        meters: number
    ) {
        // code
    }
...
}

As we'll see, we cannot do much with parameter decorators themselves, because of the minimal information received by the decorator function. That will make it important to share data between parameter decorators and other code, such as method decorators.

To use decorators, two features must be enabled in TypeScript, so be sure to review the introduction to decorators article in this series.

This article is part of a series:

Exploring Parameter Decorators in TypeScript

Parameter decorators are attached to the parameters of class constructors, or class member methods. They cannot be used with a standalone function, because you'll get the Decorators are not valid here error. They can only be used with parameters to functions that are part of a class definition, such as shown above.

The signature for parameter decorator functions is:

  1. Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
  2. A string giving the name of the property
  3. The ordinal index of the parameter in the function’s parameter list

The first two arguments are similar to the arguments supplied to property and accessor decorator functions. The third refers to the position within the parameter list of a class method:

class ClassName {
    method(param0, param1, param2, param3, ...) { .. }
}

The parameters are numbered with a 0-based index as shown here. The third argument is simply an integer giving the index, such as 0, 1, 2, etc.

With other decorators, an object called the PropertyDescriptor is presented in the third argument. There are many interesting things one can do with that descriptor, but it is not available for parameter decorators.

Simple example of Parameter Decorators in TypeScript

To see how it works, let's try a simple example that prints the values we receive:

import * as util from 'util';

function logParameter(target: Object, propertyKey: string | symbol,
                        parameterIndex: number) {

    console.log(`logParameter ${target} ${util.inspect(target)} ${String(propertyKey)} ${parameterIndex}`);
}

class ParameterExample {

    member(@logParameter x: number,
           @logParameter y: number) {
        console.log(`member ${x} ${y}`);
    }
}

const pex = new ParameterExample();
pex.member(2, 3);
pex.member(3, 5);
pex.member(5, 8);

The type for target is specified as a generic Object. The propertyKey is the name of the function, in this case member. The parameterIndex is an integer, starting from 0, enumerating the parameter to which this decorator is attached.

Running this script, we get the following output:

$ npx ts-node lib/parameters/parameters.ts 
logParameter [object Object] {} member 1
logParameter [object Object] {} member 0
member 2 3
member 3 5
member 5 8

The target turns out to be an anonymous Object. Otherwise the key and the index values are as expected.

Notice that there is no opportunity for a parameter decorator to execute code against instances of the object containing the parameter. Instead, the scope of its impact is during the time the class object is being created. Unlike with other decorators, We're not given any object that can be modified to influence behavior of class instances.

Instead, the parameter decorator serves primarily as a marker adding information to a method parameter. The official documentation clearly says this:

A parameter decorator can only be used to observe that a parameter has been declared on a method.

In most cases, doing anything significant with a parameter decorator requires cooperation with other decorators. For example, a parameter decorator can store data using the Reflection and Reflection Metadata API's, and other decorators can use that data while implementing other features.

Taking a deeper look at data available to parameter decorators

There is potentially value in deeply inspecting the target object. We see from the TypeScript documentation that it is the class object, so let's verify what that means.

In the (www.npmjs.com) decorator-inspectors package, there is a decorator we can use. This example is derived from that decorator:

export function ParameterInspector(target: Object,
                                    propertyKey: string | symbol,
                                    parameterIndex: number) {

    const ret = {
        target, propertyKey, parameterIndex,
        ownKeys: Object.getOwnPropertyNames(target),
        members: {}
    };
    for (const key of Object.getOwnPropertyNames(target)) {
        ret.members[key] = {
            obj: target[key],
            descriptor: util.inspect(
                Object.getOwnPropertyDescriptor(target, key)
            )
        };
    }
    console.log(ret);
}

It retrieves the list of property names, and then gets further details of those properties.

If we substitute @ParameterInspector for @logInspector in the example above, we get this output:

{
  target: {},
  propertyKey: 'member',
  parameterIndex: 0,
  ownKeys: [ 'constructor', 'member' ],
  members: {
    constructor: {
      obj: [class ParameterExample],
      descriptor: '{\n' +
        '  value: [class ParameterExample],\n' +
        '  writable: true,\n' +
        '  enumerable: false,\n' +
        '  configurable: true\n' +
        '}'
    },
    member: {
      obj: [Function: member],
      descriptor: '{\n' +
        '  value: [Function: member],\n' +
        '  writable: true,\n' +
        '  enumerable: false,\n' +
        '  configurable: true\n' +
        '}'
    }
  }
}

Indeed, this makes it clear that target is the class shown above. The list returned by getOwnPropertyNames is the method names - including constructor as a method, even though we did not explicitly create a constructor. There is even a PropertyDescriptor available.

Registering parameter decorators with a framework

We just discussed how it would be beneficial to save parameter decorator data somewhere, so that other decorators can do something with that data. As we said with class decorators and property decorators, your decorators could be part of a "framework" where each works together for a larger goal.

An example might be a route handling method in a class representing a Router in a web framework like Express. We might want the to inject into parameters values captured from the query string in the URL, or body parameters in a POST request.

@Router('/blog')
class BlogRouter {
    @Get('/view/:id')
    viewPost(req, res, next,
        @URLParam('id') id: string
    ) {
        // handle route
    }
}

The Reflet decorator library for Express has parameter decorators like this, as well as the other decorators shown here. For this example let's only implement the portion of URLParam that records some data. When we work with method decorators, we'll create a more complete example where method and parameter decorators work together.

const registered = [];

function URLParam(id: string) {
    return (target: Object,
        propertyKey: string | symbol,
        parameterIndex: number) => {

        const topush = {
            target, propertyKey, parameterIndex, urlparam: id,
            ownKeys: Object.getOwnPropertyNames(target),
            function: target[propertyKey],
            // funcDescriptor: Object.getOwnPropertyDescriptor(target, propertyKey)
        };
        registered.push(topush);
    }
}

class BlogRouter {

    viewPost(req, res, next,
        @URLParam('id') id: string
    ) {
        console.log(`viewPost`);
    }

    viewComments(req, res, next,
                @URLParam('id') id: string,
                @URLParam('commentID') commentID: string
    ) {
        console.log(`viewComments`);
    }
}

console.log(registered);

URLParam is a parameter descriptor function which gathers some data about the parameter decorator, and the method containing the parameter. It saves this to an array, with the intention that other decorators or a framework will use this data to construct something useful. When discussing the reflection and metadata API's we'll see a more practical way to store this array of data.

In the BlogRouter class, we have two methods, with a few parameters split between them, and some of the parameters have @URLParam decorators.

We can then run the script as so:

$ npx ts-node lib/parameters/urlparam.ts [
  {
    target: {},
    propertyKey: 'viewPost',
    parameterIndex: 3,
    urlparam: 'id',
    ownKeys: [ 'constructor', 'viewPost', 'viewComments' ],
    function: [Function: viewPost]
  },
  {
    target: {},
    propertyKey: 'viewComments',
    parameterIndex: 4,
    urlparam: 'commentID',
    ownKeys: [ 'constructor', 'viewPost', 'viewComments' ],
    function: [Function: viewComments]
  },
  {
    target: {},
    propertyKey: 'viewComments',
    parameterIndex: 3,
    urlparam: 'id',
    ownKeys: [ 'constructor', 'viewPost', 'viewComments' ],
    function: [Function: viewComments]
  }
]

This gives us three corresponding data objects. The propertyKey field contains the method name containing the parameter, while parameterIndex contains its index in the parameter list. We then record in urlparam which item to grab from the URL. And then we record the list of function names, as well as the function object for the method, because those might be useful.

What we've proved is it's very easy to record any data we like about the properties in another location.

Summary

We are able to attach decorators to method parameters. This means we can record information about the decorators attached to each parameters, and then do something with that data.

The nature of the data provided to the decorator function limits how much the function can do. This means what we'll look to is for a method decorator function to utilize data about parameter decorators to do something useful.

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)