TypeScript experimental (enabled by setting “experimentalDecorators”: true within the tsconfig.json) enables a feature known as decorators, in C# we call them attributes and Java they’re annotations. Basically they are way to extend the functionality of classes, either by adding meta data or adding code to enable AOP type functionality.
To define a decorator we simple write a function which is evaluated, then called by the runtime allow for us to write AOP etc.
A decorator function takes different arguments depending upon whether it’s used on a class, property, parameter or method. Let’s assume we were to create a logging AOP decorator, we’d use the following signatures.
Let’s assume we’re writing one of those classic AOP functions, a log function.
Class Decorators
The class decorator has a simple form, a single argument of type Function. This argument is basically our class function.
function logClass(target: Function): any {
// implementation
}
If our desire is to intercept and effect the creation of our classes then we will need to execute this Function and then in essence add our own prototypes or the likes. If our aim is to log the creation of this class, for example if we want to view performance numbers, then we can obviously log before and after creation via the decorator.
Let’s create a fairly standard looking class decorator which in this case has extended the logClass signature to be type safe
function logClass<T extends {new(...constructorArgs: any[]): any}>(ctor: T) {
const newCtor: any = function (...args: any[]) {
// pre ctor invocation
const f: any = function () {
return new ctor(...args);
}
f.prototype = ctor.prototype;
const instance: any = new f();
// post ctor invocation
return instance;
}
newCtor.prototype = ctor.prototype;
return newCtor;
}
If we want to get the name of the class we (at least in TypeScript) will need to have a line such as
const target: any = ctor;
and from this target we can now get the name of the type, for example
console.log(target.name);
In usages we simply write the following
@logClass
class MyClass {
}
Method Decorators
The method decorator is somewhat more complicated, as one would expect, as it takes three arguments, looking like this
function logMethod(
target: any, propertyKey: string,
propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
// implementation
}
The first argument is the instance of the class, the second is the method name and the final one relates to the PropertyDescriptor parameters for the method which is of the following sample shape
{
value: [Function],
writable: true,
enumerable: true,
configurable: true
}
For a usage example we might write something like this
function logMethod(target: any,
propertyKey: string,
propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
const method = propertyDescriptor.value;
propertyDescriptor.value = function (...args: any[]) {
// pre method call
const result = method.apply(this, args);
// post method call
return result;
}
return propertyDescriptor;
};
In usages we simply write the following
class MyClass {
@logMethod
run(): void {
}
}
Parameter Decorators
We can apply a decorator to an argument/parameter of a method with a decorator with the parameters show below
function logParameter(
target: any, propertyKey: string | symbol,
propertyIndex: number): void {
// implementation
}
The target is the instance of the target object, the propertyKey is the name of the method the parameter we’re decorating is on.
Note: As per TypeScript documentation on this decorator – a parameter decorator can only be used to observe that a parameter has been declared on a method.
In usages we simply write the following
class MyClass {
@logMethod
run(@logParameter options: any): void {
}
}
You’ll notice the addition of the logMethod in the example above. The thing is the parameter decorator in this case needs to work alongside the method decorator because the parameters within this decorator will simply be the instance of the object or prototype, the method name and the index of the parameter. There isn’t any way from these three parameters to directly get the value passed into the parameter, hence we’d probably tend towards using the logParameter to add a prototype or the likes to allow the logMethod to then query for these prototypes.
However we might also be able to utilities the reflect-metadata npm package to embed meta data alongside our types – we’ll look at this in another post.
Property Decorators
A property in TypeScript is a field in C# terms. The format of the function is shown below, as per other functions, the target is the object or prototype. The propertyKey is the name of the property/field the decorator’s associated with.
function logProperty(target: any, propertyKey: string): void {
// implementation
}
In usages we simply write the following
class MyClass {
@logProperty
public running: boolean;
}
We can insert our own implementation of the property’s getter/setter in the following way
function logProperty(target: any, propertyKey: string): void {
let value = target[propertyKey];
const getter = () => {
return value + " Doo";
};
const setter = (newVal: any) => {
value = newVal;
};
if (delete target[propertyKey]) {
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
In the above we create our own getter and setter, then remove the existing property replacing with our own.
Further…
Decorators are (at the time of writing this) in stage 2 proposal state, see https://github.com/tc39/proposal-decorators and are different in implementation to those experimental features within TypeScript.