In TypeScript, decorators are powerful tools that let you modify or extend the behavior of classes, methods, properties, or parameters. They work with classes to add functionality or annotations without changing the original source code directly.
If you’re familiar with Python, you might recognize some similarities, but TypeScript decorators have their own unique approach to improving code organization and reusability. This blog article will discuss decorators, their syntax, and real-world examples of how they might improve TypeScript projects.
>> Read more: Mastering Utility Types in TypeScript For Better Code with Examples
What are Decorators in TypeScript?
Decorators are a TypeScript feature that enables the application of higher-order functions to class constructors and their members. These functions, known as decorators, are prefixed with the '@' symbol and are applied using special metadata to classes and class members during the compilation process.
Syntax of TypeScript Decorators
Defining a decorator in TypeScript involves creating a function that takes either three or two parameters:
Three Parameters Syntax
function decoratorFunction(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Example implementation
console.log(`Decorating ${propertyKey} of class ${target.constructor.name}`);
}
class MyClass {
@decoratorFunction
myMethod() {
// Method implementation
}
}
target: This parameter refers to the constructor function of the class if the decorator is applied to a class, or the prototype of the class if applied to a class member (method, property, etc.). It represents the object that the decorator is applied to.propertyKey: This parameter is a string that represents the name of the class member the decorator is applied to. It can be the name of a method, property, or any other class member.descriptor: This parameter is of typePropertyDescriptorand contains information about the class member being decorated. It includes properties likevalue,writable,enumerable, andconfigurable. You can use this object to modify or inspect the behavior of the decorated member.
In this example, the decoratorFunction is applied to the myMethod of the MyClass. When myMethod is called, it will print a message like "Decorating myMethod of class MyClass" to the console.
Two Parameters Syntax (when applied to a class):
function decoratorFunction(target: any) {
// Example implementation
console.log(`Decorating class ${target.name}`);
}
@decoratorFunction
class MyClass {
// Class definition
}
target: In this case, when the decorator is applied to a class, thetargetparameter represents the constructor function of the class. It's the same as thetargetparameter in the three-parameter syntax but without the other two parameters (propertyKeyanddescriptor).
These parameters allow decorators to interact with and modify class constructors and their members in various ways. You can use the target to access the class itself and its prototype, the propertyKey to identify the specific member being decorated, and the descriptor to inspect or modify the behavior of the member.
In this example, the decoratorFunction is applied directly to the MyClass. When the class is defined, it will print a message like "Decorating class MyClass" to the console.
In TypeScript, decorators are great tools for metaprogramming; knowing these parameters will help you to create efficient decorators that may elegantly change and improve your code.
TypeScript Decorators Usage
Decorators can be used in classes, methods, properties, and parameters in TypeScript:
// Decorator for class
function classDecorator(target: Function) {
console.log(`Class Decorator: ${target.name}`);
}
// Decorator for method
function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(`Method Decorator: ${propertyKey} in class ${target.constructor.name}`);
}
// Decorator for property
function propertyDecorator(target: any, propertyKey: string) {
console.log(`Property Decorator: ${propertyKey} in class ${target.constructor.name}`);
}
// Decorator for parameter
function parameterDecorator(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter Decorator: Parameter at index ${parameterIndex} of ${propertyKey} in class ${target.constructor.name}`);
}
@classDecorator
class ExampleClass {
@propertyDecorator
classProperty: string;
constructor(@parameterDecorator private parameter: string) {}
@methodDecorator
exampleMethod() {}
}
const instance = new ExampleClass("Hello, World");
instance.exampleMethod();
In this example:
classDecoratoris applied to the class itself (ExampleClass) and logs information about the class.propertyDecoratoris applied to theclassPropertyand logs information about the property within the class.parameterDecoratoris applied to the constructor parameterparameterand logs information about the parameter.methodDecoratoris applied to theexampleMethodand logs information about the method within the class.
When you create an instance of ExampleClass, you'll see the decorators in action, logging information about the class, properties, parameters, and methods. Here's the expected output:
Class Decorator: ExampleClass
Property Decorator: classProperty in class ExampleClass
Parameter Decorator: Parameter at index 0 of parameter in class ExampleClass
Method Decorator: exampleMethod in class ExampleClass
Real-world Examples
Logging Decorator
One practical use case of decorators is logging method calls for debugging and monitoring purposes. Let's implement a simple logging decorator:
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling method: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${JSON.stringify(result)}`);
return result;
};
}
class Calculator {
@logMethod
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(5, 3); // Output will log method calls and return values.
The output of running calc.add(5, 3); will be:
Calling method: add with arguments: [5,3]
Method add returned: 8
This output demonstrates that the logMethod decorator logs information about the method call and its return value when the add method is invoked.
Authorization Decorator
Imagine you want to restrict access to certain methods based on user roles. Decorators can help enforce authorization rules in an elegant manner:
function authorize(allowedRoles: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// Check if user has allowedRoles
const userRoles: string[] = ['admin', 'moderator']; // Simulating user roles
const isAuthorized = allowedRoles.some(role => userRoles.includes(role));
if (!isAuthorized) {
throw new Error('Unauthorized! Insufficient privileges.');
}
return originalMethod.apply(this, args);
};
};
}
class AdminPanel {
@authorize(['admin'])
deleteUser(userId: string) {
// Delete user logic here...
console.log(`User with ID ${userId} deleted.`);
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUser('123'); // Authorized
>> You may be interested in: The Impacts Of No-Code AI On App Development
Decorator Metadata in TypeScript 5.2
Decorator metadata is an interesting new tool introduced in TypeScript 5.2. This function lets decorators build and consume metadata on any class they're used on or within. Decorator metadata lets you quickly attach extra details to classes, properties, or methods.
Here's a brief example of how decorator metadata works:
// Define an interface for the context object.
interface Context {
name: string; // The name of the class member.
metadata: Record<PropertyKey, unknown>; // A metadata object to store additional information.
}
// Define a decorator function called setMetadata.
// This decorator sets a flag in the metadata for a class member.
function setMetadata(_target: any, context: Context) {
// Set the metadata flag to true for the given name.
context.metadata[context.name] = true;
}
// Define a class called SomeClass.
class SomeClass {
// Apply the setMetadata decorator to the 'foo' property.
@setMetadata
foo = 123;
// Apply the setMetadata decorator to the 'bar' accessor (getter).
@setMetadata
accessor bar = "hello!";
// Apply the setMetadata decorator to the 'baz' method.
@setMetadata
baz() { }
}
// Access the metadata stored in SomeClass using Symbol.metadata.
const ourMetadata = SomeClass[Symbol.metadata];
// Output the metadata as a JSON string for inspection.
console.log(JSON.stringify(ourMetadata));
// The output will show the metadata flags for each decorated member.
// { "bar": true, "baz": true, "foo": true }
In this example, the setMetadata decorator is used to attach metadata to class members. The metadata is stored in a dictionary-like object, and it can be accessed via the Symbol.metadata property on the class.
Sometimes, you may want to specify which properties or fields of an object should be included in the serialized JSON output. That's when Metadata Decorators come into play.
In TypeScript 5.2, you can achieve this using decorator metadata. Let's consider a practical example where we want to serialize instances of a Person class to JSON but only include specific properties marked with a @serialize decorator.
// Import the necessary decorator and utility functions
import { serialize, jsonify } from "./serializer";
// Define a class where we want to apply serialization metadata
class Person {
firstName: string;
lastName: string;
@serialize
age: number;
@serialize
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
// Define a custom method to generate JSON representation
toJSON() {
return jsonify(this);
}
}
// Create an instance of the Person class
const person = new Person("John", "Doe", 30);
// Serialize the person object to JSON using the toJSON method
const serializedPerson = person.toJSON();
console.log(serializedPerson);
// Output: { "age": 30, "fullName": "John Doe" }
This is how the module ./serialize.ts might be defined:
const serializables = Symbol();
type Context =
| ClassAccessorDecoratorContext
| ClassGetterDecoratorContext
| ClassFieldDecoratorContext
;
export function serialize(_target: any, context: Context): void {
if (context.static || context.private) {
throw new Error("Can only serialize public instance members.")
}
if (typeof context.name === "symbol") {
throw new Error("Cannot serialize symbol-named properties.");
}
const propNames =
(context.metadata[serializables] as string[] | undefined) ??= [];
propNames.push(context.name);
}
export function jsonify(instance: object): string {
const metadata = instance.constructor[Symbol.metadata];
const propNames = metadata?.[serializables] as string[] | undefined;
if (!propNames) {
throw new Error("No members marked with @serialize.");
}
const pairStrings = propNames.map(key => {
const strKey = JSON.stringify(key);
const strValue = JSON.stringify((instance as any)[key]);
return `${strKey}: ${strValue}`;
});
return `{ ${pairStrings.join(", ")} }`;
}
In this example, the Person class represents an individual with properties like firstName, lastName, and age. We want to serialize instances of this class to JSON but only include properties marked with the @serialize decorator.
The @serialize decorator is applied to both the age property and the fullName getter. This decorator adds metadata to these class members, indicating that they should be included in the serialized output.
The toJSON method is defined in the Person class to generate the JSON representation of the object. It calls the jsonify function, which utilizes the decorator metadata to determine which properties should be included in the JSON output.
The result is a JSON object that includes only the age property and the fullName property, as specified by the @serialize decorator. This level of control over serialization allows you to tailor the output to your specific requirements, improving code organization and maintainability.
Decorator metadata creates opportunities for several use cases, including serialization, debugging, or decorator-based dependency injection. It lets you improve the usability and organize your code creatively.
Decorator metadata helps you to provide your classes and their members context and behavior, hence improving their usability and functionality in practical settings. If you are interested in metadata, please read more about it in the original post at this link: TypeScript 5.2 Release Notes - Decorator Metadata.
>> You may consider:
- Which Web Development Language Should You Learn in 2023?
- Web Development Tips To Enhance Your Skills
- How To Successfully Hire An Android App Developer?
Conclusion
TypeScript decorators are a powerful feature that allows developers to enhance their code with ease. Decorators can improve code readability, reusability, and maintainability for logging, authentication, and other functionalities.
Using decorators wisely will help you to produce more modular, clearer code and benefit from meta-programming. Knowing this will help you to start a path to include decorators into your TypeScript projects, therefore maintaining efficient and tidy codes. Happy coding!
>>> Follow and Contact Relia Software for more information!
- coding
- development
