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:
- Introduction to Decorators
- Class Decorators
- Property Decorators
- Accessor Decorators
- Parameter Decorators This article
- Method Decorators
- Hybrid Decorators
- Using Reflection and Reflection API with Decorators
- Runtime data validation using Decorators
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:
- Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
- A string giving the name of the property
- 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
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.