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 typePropertyDescriptor
and 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, thetarget
parameter represents the constructor function of the class. It's the same as thetarget
parameter in the three-parameter syntax but without the other two parameters (propertyKey
anddescriptor
).
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:
classDecorator
is applied to the class itself (ExampleClass
) and logs information about the class.propertyDecorator
is applied to theclassProperty
and logs information about the property within the class.parameterDecorator
is applied to the constructor parameterparameter
and logs information about the parameter.methodDecorator
is applied to theexampleMethod
and 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