7 min to read
Typescript decorators!
Typescript Decorators
How are you doing? First post January 2023! Today’s post is dedicated to the TS decorators. As always I’m sharing the official documentation here
What are they?
Functions. Nothing more and nothing less than functions that we can apply in our classes:
- Class
- Class Property
- Class Method
- Class Accessor
- Class Method Parameter
Where did they come from? Well, thanks to the class management in TS and ES6, we can now talk about decorators. They are an experimental feature in TS, and in ES they are still in stage 2 here
Syntax
Let’s look at the most basic example of a decorator:
function decorator() {
console.log("I am a decorator!");
}
@Decorator
class A {}
Decorator types
We can implement 5 types of decorators:
- Class Decorators
- Property Decorators
- Method Decorators
- Accessor Decorators
- Parameter Decorators
Let’s see an example where these 5 decorators are reflected:
@classDecorator
class User {
@propertyDecorator
name: string;
@methodDecorator
walk(
@parameterDecorator
speed: number
) {}
@accessorDecorator
get age() {}
}
Decorators Composition
We can apply multiple decorators to the same target. In that case, we will have to take into account the order of evaluation of these compound decorators, similar to the composition of functions in mathematics, to see the order of evaluation here We can decorate on one or multiple lines:
@f @g x
@f
@g
x
Method decorator
Let’s take a look at its structure:
type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
The params it receives:
- target: the constructor function of the class or instance.
- propertyKey: the name of the property.
- descriptor: the property descriptor, the config of the object.
So, we can say that what differentiates the methods decorators from, for example, the property decortators, in principle, is this param descriptor. This descriptor is what will allow us to overwrite the original implementation in order to inject our logic.
Sample 1:
//Decorator
function logger(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log("params: ", ...args);
const result = original.call(this, ...args);
console.log("result: ", result);
return result;
};
}
// method decorator
class C {
@logger
add(x: number, y: number) {
return x + y;
}
}
const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3
In this example we see that by applying the @logger decorator to the add() method of the C class, every time we invoke that method, the params and the result of the operation will be logged.
Sample 2:
Decorator:
function enumerable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
The only thing we do is to modify the enumerable prop of the property descriptor. We apply the decorator to the method of our class:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
Sample 3:
Another case that I personally found very interesting to apply, is for example, to add a cache for some method requests. In this case, we not only apply the structure and logic that corresponds to the method decorator, but we are also going to combine everything with some rxjs magic to transmit values and store them for a certain amount of time. I share with you a post with the guide to review it and/or implement it here
Composition: Parameter + Method decorators
In the example we are going to apply 2 decorators to mark params as required (@required) and to validate the args before invoking the original method (@validate) .
Example of both decorators:
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
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: any,
propertyName: string,
descriptor: TypedPropertyDescriptor<Function>
) {
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);
};
}
Implementation:
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
@validate // method decorator
print(@required verbose: boolean) {
// param decorator
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}
Advantages
Decorators help us to improve our code with a declarative, easy to read syntax. With a single line we can apply logic and functions without the need to write additional code.
It is interesting to analyze which cases and scenarios of our projects, may be candidates to implement these decorators, especially when we notice some logic that we can abstract and reuse.
References
Post and complementary info with more examples here Happy coding!