Enabling decorators in TypeScript
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
Simple decorator example
function logClassName(target: any) { // target is the constructor function of the class console.log(`Class name: ${target.name}`); } @logClassName class MyClass { // ... } const myClassInstance = new MyClass();
As you can see this is a very simple example. Usually we want more, for example
- we want to pass values to our decorator. We do this by creating a decorator factory (more below)
- we want to access values from other decorators. We do this by using reflection metadata (more below).
The role of Reflection in decorators
Reflaction is used to temporarily store values. Metadata Reflection API is experimental and not yet part of the ECMAScript standard. This means that its behavior may change in future versions of TypeScript and it may not be supported by all JavaScript environments. To use Reflect.metadata
and other functions from the Metadata Reflection API in TypeScript, you need to install the reflect-metadata
package and import it at the top of your TypeScript file.
import "reflect-metadata"; @Reflect.metadata("classKey", "classValue") class MyClass { @Reflect.metadata("methodKey", "methodValue") myMethod() { // ... } } const classMetadataValue = Reflect.getMetadata("classKey", MyClass); console.log(classMetadataValue); // "classValue" const methodMetadataValue = Reflect.getMetadata("methodKey", MyClass.prototype.myMethod); console.log(methodMetadataValue); // "methodValue"
Class decorator
In the following example the class decorator adds a property to a class.
function myClassDecorator<T extends { new(...args: any[]): {} }>(constructor: T) { return class extends constructor { propB = "B"; }; } @myClassDecorator class MyClass { public propA = "A"; } const ex = new MyClass(); console.log(ex.propA); // output is 'A' // @ts-ignore // TypeScript still gives us error: Property 'decoratorProp' does not exist on type 'Example', // because the decorator does not change the type 'Example'. So we just ignore it to show, that // we can call playground.propB successfully anyway. console.log(ex.propB); // output is 'B'
Method decorator (using decorator factory)
A decorator factory is a function that returns the decorator function. We use it to allow passing in data (‘num’ in this case). @add
is called first, then @subtract
and finally myMethod.
// A decorator factory is a function that returns the decorator function. // We use it to allow passing in data ('num' in this case). // Must return 'any' to prevent 'Unable to resolve signature of method decorator when called as an expression' function add(num: number): any { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { // target is the prototype of the class // propertyKey is the name of the method const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`add ${num}`); return originalMethod.apply(this, args) + num; } }; } function subtract(num: number): any { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`subtract ${num}`); return originalMethod.apply(this, args) - num; } }; } class MyClass { @add(6) @subtract(3) myMethod(num: number) { console.log("myMethod " + num * 2); return num * 2; } } const myInstance = new MyClass(); const result = myInstance.myMethod(5); console.log(result); export {}
add 6 subtract 3 myMethod 10 13
Parameter decorator
In the following example we a method decorator @validate
and a parameter decorator @required
which checks if a property has a value. We use Reflect.defineMetadata
to store the index of the parameter that we defined as required in an array. For example, when writing print(@required verbose?: boolean)
we mark the parameter at index 0 as required. In the @validate method we use Reflect.getOwnMetadata
to read the value and do the actual check.
So this example shows how two decorators work together and how they communicate by using functionality from import "reflect-metadata"
.
import "reflect-metadata"; const requiredMetadataKey = Symbol("required"); class BugReport { type = "report"; title: string; constructor(t: string) { this.title = t; } @validate print(@required verbose?: boolean, second?: string, third?: string) { if (verbose) { return `type: ${this.type}\ntitle: ${this.title}`; } else { return this.title; } } } function required(target: Object, propertyKey: string | symbol, parameterIndex: number) { let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; existingRequiredParameters.push(parameterIndex); Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey); } function validate(target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<(verbose: boolean) => string>) { let method = descriptor.value!; descriptor.value = function () { let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName); if (requiredParameters) { for (let parameterIndex of requiredParameters) { if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) { throw new Error("Missing required argument."); } } } return method.apply(this, arguments as any); }; } const report = new BugReport("the title"); console.log(report.print(true, 'B', 'C')); // type: report // title: the title console.log(report.print(false)); // the title console.log(report.print()); // Error: Missing required argument
Property decorator
In the following example we define @format("Hello, %s")
on class property name
. The decorator format
uses Reflect.metadata
which is a default metadata decorator factory. We use it to define metadata, namely the format string "Hello, %s"
stored under key Symbol("format")
. We can then use Reflect.getMetadata(formatMetadataKey, target, key)
to retrieve the metadata value.
import "reflect-metadata"; const formatMetadataKey = Symbol("format"); function format(formatString: string) { return Reflect.metadata(formatMetadataKey, formatString); } function getFormat(target: any, key: string | symbol) { return Reflect.getMetadata(formatMetadataKey, target, key); } class MyClass { @format("Hello, %s") name: string = ""; greet() { let formatString = getFormat(this, "name"); return formatString.replace("%s", this.name); } } const ex = new MyClass() ex.name = "Mathias"; console.log(ex.greet()); export {};
Accessor decorator
In the following example we use @configurable
to define whether an accessor should be configurable or not.
class Point { private _x: number; private _y: number; constructor(x: number, y: number) { this._x = x; this._y = y; } @configurable(false) get x() { return this._x; } @configurable(false) get y() { return this._y; } } function configurable(value: boolean) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.configurable = value; }; } const point = new Point(1, 2); console.log(point.y); export {}