Site icon DataFlair

Typescript Decorators

typescript decorators

Job-ready Online Courses: Knowledge Awaits – Click to Access!

TypeScript is known for its powerful type system that helps developers catch errors early in the development cycle. However, TypeScript also supports a feature called decorators, which is not found in vanilla JavaScript.

Decorators allow you to add metadata or behavior to classes, methods, and properties at runtime. They are a powerful way to enhance the functionality of your code without cluttering it with extra logic. In this article, we’ll explore TypeScript decorators and their use cases in detail.

Types of TypeScript Decorators

TypeScript decorators can be divided into four types: class decorators, method decorators, property decorators, and parameter decorators.

1. Class Decorators

Class decorators are applied to the class definition and are used to modify or extend the class functionality. They are declared just before the class declaration and are denoted by the @ symbol.

Here’s an example of a class decorator:

function DataFlair_logClass(target: Function) {
 console.log(target);
}


@DataFlair_logClass
class MyClass {
 // class implementation
}

In this example, the DataFlair_logClass function is a class decorator that logs the class definition to the console. The @DataFair_logClass decorator is applied to the MyClass class, and when the class is instantiated, the DataFlair_logClass function is executed. Then the class definition is logged into the console.

2. Method Decorators

Method decorators are applied to the method definition and are used to modify or extend the method functionality. They are declared just before the method declaration and are denoted by the @ symbol.

Here’s an example of a method decorator:

function DataFlair_logMethod(
 target: Object,
 key: string,
 descriptor: PropertyDescriptor
) {
 console.log(target);
 console.log(key);
 console.log(descriptor);
}


class MyClass {
 @DataFlair_logMethod
 myMethod() {
   // method implementation
 }
}

In this example, the DataFlair_logMethod function is a method decorator that logs the object that the method belongs to, the method name, and the method descriptor. The @DataFlair_logMethod decorator is applied to the myMethod method of the MyClass class. When the method is called, the DataFlair_logMethod function is executed, and the object, method name, and descriptor are logged to the console.

3. Property Decorators

Property decorators are applied to the property definition and are used to modify or extend the property functionality. They are declared just before the property declaration and are denoted by the @ symbol.

Here’s an example of a property decorator:

function DataFlair_logProperty(target: Object, key: string) {
 console.log(target);
 console.log(key);
}


class MyClass {
 @DataFlair_logProperty
 myProperty: string;
}

In this example, the DataFlair_logProperty function is a property decorator that logs the object that the property belongs to and the property name. The @DataFlair_logProperty decorator is applied to the myProperty property of the MyClass class. When the property is accessed, the DataFlair_logProperty function is executed, and the object and property name are logged to the console.

4. Parameter Decorators

Parameter decorators are applied to the parameter definition and are used to modify or extend the parameter functionality. They are declared just before the parameter declaration and are denoted by the @ symbol.

Here’s an example of a parameter decorator:

function DataFlair_logParameter(target: Object, key: string, index: number) {
 console.log(target);
 console.log(key);
 console.log(index);
}


class MyClass {
 myMethod(
   @DataFlair_logParameter param1: string,
   @DataFlair_logParameter param2: number
 ) {
   // method implementation
 }
}

In this example, the DataFlair_logParameter function is a parameter decorator that logs the object that the parameter belongs to, the method name, and the parameter index. The @DataFlair_logParameter decorator is applied to the param1 and param2 parameters of the myMethod method of the MyClass class. When the method is called, the DataFlair_logParameter function is executed. The object, method name, and parameter index are logged into the console.

Decorators with Arguments

Decorators can also take arguments that allow you to customize their behavior. To define a decorator with arguments, you need to return a function that takes the target object as its first parameter and returns the decorator function. Here’s an example of a class decorator with arguments:

function DataFlair_logName(name: string) {
 return function (target: Function) {
   console.log(`Class name: ${name}`);
 };
}


@DataFlair_logName('MyClass')
class MyClass {
 // class implementation
}

Output

Class name: Decorator Class

In this example, the logName function returns a decorator function that logs the class name with the provided argument. The @logName(‘MyClass’) decorator is applied to the MyClass class, and when the class is instantiated, the decorator function is executed, and the class name is logged to the console.

Using Decorators for Dependency Injection

One of the most powerful use cases of TypeScript decorators is for dependency injection. Dependency injection is a programming pattern in which objects are passed into a class constructor rather than being instantiated within the class. This makes it easier to manage dependencies and test classes in isolation.

Here’s an example of a simple dependency injection using decorators:

interface DataFlair_MyServiceInterface {
 getValue(): number;
}


class MyService implements DataFlair_MyServiceInterface {
 getValue(): number {
   return 42;
 }
}


class MyClass {
 private myService: DataFlair_MyServiceInterface;


 constructor(@Inject(MyService) myService: DataFlair_MyServiceInterface) {
   this.myService = myService;
 }
}

Use Cases of TypeScript Decorators

Now that we’ve seen the four types of TypeScript decorators, let’s explore some use cases where decorators can be used to enhance your code.

1. Logging

One common use case for decorators is logging. Adding a logging decorator to a class, method, or property lets you log relevant information to the console. Some of the examples are the class or method name, the time of execution, or the input and output parameters.

Here’s an example of a logging decorator:

function DataFlair_log(
 target: Object,
 key: string,
 descriptor: PropertyDescriptor
) {
 const originalMethod = descriptor.value;


 descriptor.value = function (...args: any[]) {
   console.log(`Calling ${key} with arguments: ${args}`);
   const result = originalMethod.apply(this, args);
   console.log(`Returned value: ${result}`);
   return result;
 };


 return descriptor;
}


class MyClass {
 @DataFlair_log
 myMethod(param1: string, param2: number) {
   return `Hello ${param1}, the answer is ${param2}`;
 }
}

Output

Calling myMethod with arguments: World,42

In this example, the DataFlair_log decorator is applied to the myMethod method of the MyClass class. The decorator modifies the method descriptor to DataFlair_log the method name and arguments before and after execution. When the method is called, the decorator logs the input arguments and the returned value to the console.

2. Validation

Another use case for decorators is validation. By adding a validation decorator to a class, method, or property, you can check the validity of the input or output data and throw an error if the data is invalid.

Here’s an example of a validation decorator

function DataFlair_validate(
 target: Object,
 key: string,
 descriptor: PropertyDescriptor
) {
 const originalMethod = descriptor.value;


 descriptor.value = function (...args: any[]) {
   const param1 = args[0];
   const param2 = args[1];


   if (typeof param1 !== 'string' || typeof param2 !== 'number') {
     throw new Error('Invalid arguments');
   }


   return originalMethod.apply(this, args);
 };


 return descriptor;
}


class MyClass {
 @DataFlair_validate
 myMethod(param1: string, param2: number) {
   return `Hello ${param1}, the answer is ${param2}`;
 }
}

Output

Hello World, the answer is 42

In this example, the DataFlair_validate decorator is applied to the myMethod method of the MyClass class. The decorator modifies the method descriptor to validate the input arguments and throws an error if they are not of the expected type. When the method is called, the decorator checks the types of input arguments and throws an error if they are invalid.

3. Authorization

A third use case for decorators is an authorization. Adding an authorization decorator to a class, method, or property lets you check if the user has the required permissions to access the resource.

Here’s an example of an authorization decorator:

function DataFlair_authorize(roles: string[]) {
 return function(target: Object, key: string, descriptor: PropertyDescriptor) {
   const originalMethod = descriptor.value;


   descriptor.value = function(...args: any[]) {
     const userRoles = ['admin', 'editor'];
    
     if (!userRoles.some(role => roles.includes(role))) {
       throw new Error
('Unauthorized access');
}  return originalMethod.apply(this, args);
}


return descriptor;
}
}


class MyClass {
@DataFlair_authorize(['admin'])
myMethod(param1: string, param2: number) {
return Hello ${param1}, the answer is ${param2};
}
}

In this example, the DataFlair_authorize decorator takes an array of roles as an argument and returns a decorator function. It modifies the method descriptor to check if the user has the required role before executing it. When the method is called, the decorator checks if the user has the required role and throws an error if they don’t.

4. Caching

A fourth use case for decorators is caching. By adding a caching decorator to a method, you can cache the results of the method and return the cached value instead of executing the method again if the input parameters are the same.

Here’s an example of a caching decorator:

function DataFlair_cache(
 target: Object,
 key: string,
 descriptor: PropertyDescriptor
) {
 const originalMethod = descriptor.value;
 const cache = new Map();


 descriptor.value = function (...args: any[]) {
   const key = JSON.stringify(args);


   if (cache.has(key)) {
     console.log(`Returning cached value for ${key}`);
     return cache.get(key);
   }


   const result = originalMethod.apply(this, args);
   cache.set(key, result);


   return result;
 };


 return descriptor;
}


class MyClass {
 @DataFlair_cache
 myMethod(param1: string, param2: number) {
   console.log('Executing myMethod...');
   return `Hello ${param1}, the answer is ${param2}`;
 }
}

Output

Executing myMethod…
Hello World, the answer is 42

In this example, the DataFlair_cache decorator is applied to the myMethod method of the MyClass class. The decorator modifies the method descriptor to cache the method results and return the cached value instead of executing the method again if the input parameters are the same. When the method is called, the decorator checks if the cached value exists for the input parameters and returns it if it does, or executes the method and caches the result if it doesn’t.

Decorator Composing in TypeScript

In TypeScript, decorators provide a way to add additional functionality to classes, methods, and properties. One way to use decorators is by composing them together to create more complex behaviors. This is known as decoration composing.

Decoration composing involves using multiple decorators on a single class, method, or property. Each decorator adds its own functionality, and the result is a combination of all the decorators’ behavior.

For example, let’s say we have a simple class DataFlair_Person that represents a person with a name and age:

class DataFlair_Person {
 constructor(public name: string, public age: number) {}
}

We can then use decorators to add additional functionality to this class. For example, we might want to add validation to the age property:

function DataFlair_validateAge(target: any, key: string) {
 let value = target[key];


 const getter = function () {
   return value;
 };


 const setter = function (newVal: number) {
   if (newVal < 0) {
     throw new Error('Age cannot be negative');
   } else {
     value = newVal;
   }
 };


 Object.defineProperty(target, key, {
   get: getter,
   set: setter,
   enumerable: true,
   configurable: true,
 });
}


class Person {
 constructor(public name: string, @DataFlair_validateAge public age: number) {}
}

In this example, we define a decorator DataFlair_validateAge that checks if the age is negative and throws an error if it is. We then apply this decorator to the age property of the DataFlair_Person class using the @DataFlair_validateAge syntax.

We can then compose this decorator with other decorators to create more complex behaviors. For example, we might want to add logging to the name property:

function DataFlair_log(target: any, key: string) {
 let value = target[key];


 const getter = function () {
   console.log(`Getting ${key} = ${value}`);
   return value;
 };


 const setter = function (newVal: string) {
   console.log(`Setting ${key} = ${newVal}`);
   value = newVal;
 };


 Object.defineProperty(target, key, {
   get: getter,
   set: setter,
   enumerable: true,
   configurable: true,
 });
}


class Person {
 @DataFlair_log
 get name() {
   return this._name;
 }


 @validateAge
 set age(value: number) {
   this._age = value;
 }


 constructor(private _name: string, private _age: number) {}
}

In this example, we define a decorator DataFlair_log that logs when the name property is read or written. We then apply this decorator to the name property using the @DataFlair_log syntax.

We also apply the DataFlair_validateAge decorator to the age property, as in the previous example.

Overall, decoration composing in TypeScript allows you to create complex behaviors by combining multiple decorators. This can make your code more modular, more reusable, and easier to maintain.

Factories in TypeScript

In TypeScript, decorators provide a way to add behavior to classes, methods, and properties. One way to use decorators is by creating factories, which are functions that return decorator functions.

A factory function takes arguments and returns a decorator function. The decorator function then takes a target (either a class constructor, a method, or a property), modifies it, and returns it. This allows us to create more flexible decorators that can be customized based on their usage.

For example, let’s say we want to create a logging decorator that logs the name and arguments of a method. We can create a factory function that takes a message as an argument and returns a decorator function:

function DataFlair_logFactory(message: string) {
 return function (
   target: any,
   propertyKey: string,
   descriptor: PropertyDescriptor
 ) {
   const originalMethod = descriptor.value;


   descriptor.value = function (...args: any[]) {
     console.log(`${message}: ${propertyKey}(${args.join(', ')})`);
     const result = originalMethod.apply(this, args);
     return result;
   };


   return descriptor;
 };
}

In this example, we define a factory function DataFlair_logFactory that takes a message as an argument and returns a decorator function. The decorator function takes a target, property key, and property descriptor as arguments and modifies the method by logging a message before and after it is called.

We can then use this factory to create customized logging decorators for different methods:

class Calculator {
 @DataFlair_logFactory('Adding')
 add(a: number, b: number) {
   return a + b;
 }


 @DataFlair_logFactory('Multiplying')
 multiply(a: number, b: number) {
   return a * b;
 }
}

In this example, we create two methods add and multiply in a Calculator class and apply the DataFlair_logFactory decorator using different messages.

Overall, using factories in decorators allows us to create more flexible and customizable decorators that can be reused across different classes and methods.

Use cases for TypeScript decorators

TypeScript decorators provide a way to add behavior to classes, methods, and properties at runtime. They are powerful tools that can be used in many different ways. Here are some common use cases for TypeScript decorators:

1. Logging: Decorators can be used to log method calls, property accesses, or class instantiations. This can be helpful for debugging or performance analysis.

2. Validation: Decorators can be used to validate inputs or outputs of methods or properties. This can help to ensure that the code behaves correctly and that errors are caught early.

3. Authorization: Decorators can be used to enforce access control policies for methods or properties. This can help to ensure that only authorized users can access certain parts of the code.

4. Caching: Decorators can be used to cache the results of expensive method calls or property accesses. This can help to improve performance and reduce redundant computations.

5. Dependency Injection: Decorators can be used to inject dependencies into classes or methods. This can help to decouple components and make the code more modular and reusable.

6. Testing: Decorators can be used to add additional behavior to classes or methods for testing purposes. For example, a decorator could be used to mock a service or to record method calls for later inspection.

Overall, TypeScript decorators provide a lot of flexibility and can be used in many different ways. They can be especially useful in large codebases or in complex systems where behavior needs to be added dynamically at runtime.

Automatic Error Guard

In TypeScript, an “automatic error guard” is a feature that allows the type system to automatically narrow the type of a variable when it is used in an error handler. This feature is also known as “control flow analysis”.

When an error is thrown in a try-catch block, the catch block is executed with an argument that represents the error. TypeScript uses this argument to automatically narrow the type of the variable based on the type of the error that was thrown.

For example, consider the following code:

try {
 // some code that may throw an error
} catch (error) {
 console.log(error.message);
}

In this code, the error variable has an implicit type of any. However, TypeScript automatically narrows the type of error to the type of the error that was thrown. If the thrown error is a SyntaxError, for example, then TypeScript will narrow the type of error to SyntaxError.

This allows us to write more type-safe code and avoid type errors that could occur if we were to assume that the error variable has a certain type.

Automatic error guards can also be used with type guards and other features of the type system to provide even more type safety and correctness in our code. Overall, automatic error guards are a powerful feature of TypeScript that help to ensure that our code is more reliable and robust.

Conclusion

TypeScript decorators are a powerful tool that can enhance your code by adding additional functionality, such as logging, validation, authorization, and caching. Decorators allow you to modify the behavior of classes, methods, and properties without changing their implementation, making them a great tool for code reuse and abstraction.

In this article, we’ve covered the four types of TypeScript decorators, their syntax, and some common use cases for decorators. We’ve also provided some code examples to illustrate the concepts discussed in the article.

Exit mobile version