TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at scale. TypeScript was developed to help solve some of the problems that arise when writing code using JavaScript. TypeScript overcomes the pitfalls of JavaScript through the use of types.
Each value in a typescript source code has a type. TypeScript checks to ensure that each value adheres to the rules associated with its type. TypeScript checks for errors in your source code without you having to execute or run your program.
This is called static type checking, which involves checking for errors in development based on the type of values used in a program.
Aside from helping you write more clear and readable code and providing you with static type checking, TypeScript offers additional features to help with code readability, reusability, and maintainability. One such feature is TypeScript decorators.
TypeScript Decorators
Decorators in TypeScript are a feature that allows you to alter or enhance the behavior of your code at runtime or add metadata to your code. Decorators allow for metaprogramming in TypeScript. Metaprogramming is a programming technique where programs can treat other programs as their data and thus modify their behavior.
Essentially, decorators are functions that will be called to execute some logic at runtime when the decorated elements are accessed or modified.
This way, additional functionality can be added to the decorated elements. TypeScript decorators can be attached to class declarations, methods, properties, accessors (getters and setters), and method parameters.
In TypeScript, decorators are prefixed with the @ symbol, and they take the form of @expression with expression evaluating to a function that will called at runtime. The general syntax for using decorators in TypeScript is as shown below:
@decoratorName
itemToDecorate
An example of a simple class decorator is shown below:
function logClass(target: Function) {
console.log("The Log Class Decorator has been called")
console.log("Class:", target);
}
@logClass // @logClass is a decorator
class MyClass {
constructor() {
console.log("An instance of MyClass has been created");
}
}
const myInstance = new MyClass();
The result of executing the code above is shown below:
Output:
The Log Class Decorator has been called
Class: [class MyClass]
An instance of MyClass has been created
The function logClass() takes in a single argument called target of type Function. The argument target is of type Function because it will receive the constructor of the class we are decorating.
To use logClass() as a decorator to decorate the class called MyClass we put @logclass just before the declaration of MyClass. The decorator must have the same name as the function you want to use to decorate an element.
When an instance of MyClass is created, the decorator’s behavior is executed in addition to the class constructor as shown by the output.
Decorators are currently available on Typescript as an experimental feature. Therefore, for you to use decorators in TypeScript, you need to enable the experimentalDecorators in the compiler option in the tsconfig.json file.
To do this, generate a tsconfig.json file in your TypeScript project folder by executing the following command in the terminal opened in your project directory:
tsc --init
Once you have a tsconfig.json file, open it and uncomment experimentalDecorators as shown below:
Additionally, set your JavaScript target version to at least ES2015.
Importance of TypeScript Decorators
Good code can easily be determined by how readable, reusable, and maintainable the code is. Readable code is code that is easy to understand and interpret and clearly communicates the developer’s intent to whoever is reading the code.
Reusable code, on the other hand, is code that allows specific components, such as functions and classes, to be repurposed, adapted, and used in other parts of an application or a completely new application without significant modification.
Maintainable code is code that can easily be modified, updated, and fixed over the course of its lifetime.
TypeScript decorators allow you to achieve code readability, reusability, and maintainability. First, TypeScript decorators allow you to enhance the behavior of your code using a declarative syntax that is easier to read. You can encapsulate logic in decorators and invoke them by decorating different elements of your code where you need the logic.
This makes your code easy to read and understand what is going on. Decorators clearly communicate a developer’s intent.
Decorators are not single-use elements; they are reusable by nature. You can create a decorator once and use it multiple times in multiple areas.
Therefore you can define decorators, import them, and use them anywhere in your code base where you want to modify the behavior of your code. This is beneficial as it allows you to avoid duplicating logic in your code base, thus enhancing the reusability of your code.
Decorators also give you a great deal of flexibility and modularity in your code, allowing you to separate different functionality into independent components. This, coupled with the fact that they allow the writing of readable and reusable code, means that TypeScript decorators will allow you to have code that is easy to maintain.
Types of TypeScript Decorators
As noted earlier, TypeScript decorators can be attached to classes, class properties, class methods, class accessors, and class method parameters. From the elements we can decorate, we get the different types of TypeScript decorators. These decorators include:
#1. Class Decorator
A class decorator is a decorator used to observe, modify, or replace a class definition. It is declared just before the class it is decorating. A class decorator is applied to the constructor of the class it is decorating. At runtime, a class decorator will be called with the constructor of the class it is decorating as its only argument.
A class decorator that is used to prevent a class from being extended is shown below:
function frozen(target: Function) {
Object.freeze(target);
Object.freeze(target.prototype)
}
@frozen
class Vehicle {
wheels: number = 4;
constructor() {
console.log("A vehicle has been created")
}
}
class Car extends Vehicle {
constructor() {
super();
console.log("A car has been created");
}
}
console.log(Object.isFrozen(Vehicle));
To prevent a class from being extended, we use the function Object.freeze() and pass in the class that we want to freeze. A decorator is used to add this functionality to a class. We can check if the Vehicle class is frozen at runtime by passing the class into isFrozen(), the output of the code is shown below:
true
#2. Property Decorator
A property decorator is used to decorate a class property, and it is declared just before the property decoration. Property decorators can be used to change or observe a property definition.
At runtime, the decorator will be called, and it takes in two arguments. First, either the constructor function of the class if the member is static or the prototype of the class in case it is an instance member. The second argument is the name of the member, that is, the property you are decorating.
In TypeScript, static members have the keyword static before them. Static members can be accessed without instantiating a class. The instance members don’t have the keyword static before them and can only be accessed after an instance of a class has been created.
An example of a property decorator is shown below:
function wheelsDecorator(target: any, propertyName: string) {
console.log(propertyName.toUpperCase())
}
class Vehicle {
@wheelsDecorator
wheels: number = 4;
constructor() {
console.log("A vehicle has been created")
}
}
The output of running the code is shown below:
WHEELS
#3. Method Decorator
A method decorator, declared just before a method declaration is a decorator used to observe, modify, or replace a method definition. It takes in three arguments, the constructor function of the class in case the member is static or the property of the class if it is an instance member.
The second argument is the name of the member and, finally, a property descriptor for the member. A property descriptor is an object associated with object properties, and it provides information about a property’s attributes and behavior.
Method decorators come in handy when you want to perform some action before or after a method is called. We can also use them to log information about the method being called. This can be useful in case you want to inform a user that a method is deprecated; that is, it is still available for use, but it is not recommended that you use it as it may be removed later.
An example of a method decorator is shown below:
const logDeprecated =(target: any, methodName: string, descriptor: PropertyDescriptor) => {
console.log(`${methodName} has been deprecated`)
console.log(descriptor);
}
class Vehicle {
wheels: number = 4;
constructor() {
console.log("A vehicle has been created")
}
@logDeprecated
reFuel(): void {
console.log("Your vehicle is being refuelled");
}
}
Output:
reFuel has been deprecated
{
value: [Function: reFuel],
writable: true,
enumerable: false,
configurable: true
}
#4. Accessor Decorators
In TypeScript, there are two types of accessor methods, get and set. Accessor methods are used to control access to class properties. Accessor decorators are used to decorate these two accessor methods, and they are declared just before an accessor declaration. Since accessors are still methods, accessor decorators work just like method decorators.
An example of an accessor decorators is shown below:
const logWheels =(target: any, accessorName: string, descriptor: PropertyDescriptor) => {
console.log(`${accessorName} used to get the number of wheels`)
console.log(descriptor);
}
class Vehicle {
private wheels: number = 4;
constructor() {
console.log("A vehicle has been created")
}
@logWheels
get numWheels(): number {
return this.wheels;
}
}
Output:
numWheels used to get the number of wheels
{
get: [Function: get numWheels],
set: undefined,
enumerable: false,
configurable: true
}
With accessor decorators, it is important to note that decorators cannot be applied to multiple get/set accessors of the same name. For instance, in our code sample above, if you create a setter called set numWheels, you can’t use the logWheels decorator on it.
#5. Parameter Decorators
A parameter decorator is used to observe that a parameter has been declared on a method, and it is declared before a parameter declaration. Parameter decorators take in three arguments, first, the constructor function of the class for a static member or the prototype of the class for an instance member.
The second argument is the name of the member, that is, the parameter name. The third argument is the ordinal index of the parameter in the function’s parameter list. That is, in the parameter list, what position is the parameter, with the first parameter being at index 0?
An example of a parameter decorator is shown below:
const passengerLog = (target: Object, propertyKey: string, parameterIndex: number) => {
console.log(`Decorator on ${propertyKey}'s paremeter index ${parameterIndex}`);
}
class Vehicle {
private wheels: number = 4;
constructor() {
console.log("A vehicle has been created")
}
pickPassenger( location: string, numPassengers: string, @passengerLog driver: string) {
console.log(`${numPassengers} picked at ${location} by ${driver}`)
}
dropPassenger(driver: string, @passengerLog location: string, numPassengers: string) {
console.log(`${numPassengers} dropped at ${location} by ${driver}`)
}
}
Output:
Decorator on pickPassenger's paremeter index 2
Decorator on dropPassenger's paremeter index 1
Conclusion
TypeScript decorators go a long way in enhancing your code readability and helping you write modular, reusable code, as you can declare decorators once and use them many times. Additionally, decorators contribute to the overall maintainability of your code.
As much as they’re still an experimental feature, decorators are very useful, and you should definitely consider familiarising yourself with them.
You may also read how to convert a string into a number in TypeScript.