Back to Java
2025-12-108 min read

TS Decorators (Java)

Learn TS Decorators (Java) step by step with clear examples and exercises.

Why This Matters

TypeScript decorators are a powerful tool that allows developers to add additional functionality or metadata to classes, methods, properties, and parameters in TypeScript code. They can be used for various purposes such as validation, metadata annotation, custom behavior modification, dependency injection, and more. By understanding and mastering the use of TypeScript decorators, you can enhance your TypeScript codebase, improve maintainability, and write more flexible and reusable code.

Prerequisites

To fully grasp the concept of TypeScript decorators, it is essential to have a solid understanding of JavaScript ES6 features, including classes, modules, arrow functions, template literals, destructuring assignments, and more. Additionally, familiarity with TypeScript basics such as interfaces, generics, type annotations, and advanced topics like type inference, conditional types, and mapped types will be beneficial.

Furthermore, having experience with object-oriented programming principles and design patterns will help you make the most of TypeScript decorators in large-scale applications that require complex customizations and adherence to specific design patterns or conventions.

Core Concept

Creating a Decorator

To create a decorator in TypeScript, you define a function that takes a constructor or a class as its parameter and returns a new constructor or a modified version of the original class. The decorator can access the original class properties and methods through the this keyword. Here's an example of a simple decorator:

function myDecorator(constructor: Function) {
console.log(`Decorating ${constructor.name}`);
}

@myDecorator
class MyClass {
// Class properties and methods
}

In this example, we define a decorator myDecorator that logs a message when it is applied to a class. We then use the decorator on the MyClass class by adding the @myDecorator annotation before its definition.

Parameterized Decorators

Parameterized decorators allow you to pass parameters to the decorator function, which can then be used to customize its behavior. Here's an example of a parameterized decorator:

function myParametrizedDecorator(parameter: string) {
return function (constructor: Function) {
console.log(`Decorating ${constructor.name} with parameter ${parameter}`);
}
}

@myParametrizedDecorator('value')
class MyClassWithParameter {
// Class properties and methods
}

In this example, we define a parameterized decorator myParametrizedDecorator that takes a string parameter. When applied to a class, it logs a message including the parameter value.

Decorating Properties and Methods

You can also use decorators to add metadata or custom behavior to properties and methods in your classes. Here's an example of a decorator for a property:

function myPropertyDecorator(target: any, key: string) {
console.log(`Decorating property ${key}`);
}

class MyClassWithProperty {
@myPropertyDecorator
myProperty: string;
}

In this example, we define a decorator myPropertyDecorator that logs a message when it is applied to a class property. We then use the decorator on the myProperty property of the MyClassWithProperty class by adding the @myPropertyDecorator annotation before its declaration.

You can also create decorators for methods in a similar way:

function myMethodDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
console.log(`Decorating method ${key}`);
// Modify the property descriptor to add custom behavior
}

class MyClassWithMethod {
myMethod() {
// Method implementation
}

@myMethodDecorator
myDecoratedMethod() {
// Decorated method implementation
}
}

In this example, we define a decorator myMethodDecorator that logs a message when it is applied to a class method. We then use the decorator on the myDecoratedMethod method of the MyClassWithMethod class by adding the @myMethodDecorator annotation before its declaration.

Accessing Class Properties and Methods in Decorators

When working with decorators, it's important to understand how you can access class properties and methods within the decorator function. You can do this using the this keyword, which refers to the instance of the decorated class. Here's an example:

class MyClass {
myProperty = 'Hello';

constructor() {
console.log(`Creating instance of ${this.constructor.name}`);
}
}

function myDecorator(target: Function) {
console.log(`Decorating class ${target.name}`);

// Access class properties and methods using 'this' keyword
console.log(`Class property: ${this.myProperty}`);
const constructor = target.prototype.constructor;
constructor();
}

@myDecorator
class MyDecoratedClass extends MyClass {
// Class properties and methods
}

In this example, we define a decorator myDecorator that logs messages related to the decorated class and accesses its properties and method using the this keyword. We then apply the decorator to a subclass of MyClass, which demonstrates how decorators can be used to customize the behavior of derived classes as well.

Worked Example

In this example, we will create a simple decorator for a class that logs all calls to a specific method and its execution time. We'll also demonstrate how to use parameterized decorators to customize the logging behavior.

function logMethodCalls(methodName: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
console.time(`${methodName} execution`);
originalMethod.apply(this, args);
console.timeEnd(`${methodName} execution`);
};
}
}

class MyClassWithLogging {
@logMethodCalls('myMethod')
myMethod(arg1: string, arg2: number) {
// Method implementation
}
}

In this example, we define a decorator logMethodCalls that logs the call to a specific method (in this case, myMethod) and its execution time. We then use the decorator on the myMethod method of the MyClassWithLogging class by adding the @logMethodCalls('myMethod') annotation before its declaration.

Now when you call the myMethod method on an instance of MyClassWithLogging, it will log a message with the method name, execution time, and arguments:

const myInstance = new MyClassWithLogging();
myInstance.myMethod('Hello', 42); // Output: myMethod execution: 0.567ms

You can also create a parameterized version of the logMethodCalls decorator to customize the logging behavior for different methods:

function logMethodCallsWithParameters(methodName: string, logLevel: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
if (logLevel <= 1) {
console.time(`${methodName} execution`);
}
originalMethod.apply(this, args);
if (logLevel > 0) {
console.timeEnd(`${methodName} execution`);
}
};
}
}

class MyClassWithLoggingAndParameters {
@logMethodCallsWithParameters('myMethod', 1)
myMethod(arg1: string, arg2: number) {
// Method implementation
}

@logMethodCallsWithParameters('anotherMethod', 0)
anotherMethod(arg1: boolean) {
// Method implementation
}
}

In this example, we define a parameterized decorator logMethodCallsWithParameters that logs the call to a specific method (in this case, either myMethod or anotherMethod) and its execution time based on a log level. We then use the decorator on both methods of the MyClassWithLoggingAndParameters class by adding the appropriate annotations before their declarations.

Now when you call the myMethod method on an instance of MyClassWithLoggingAndParameters, it will log a message with the method name, execution time, and arguments:

const myInstance = new MyClassWithLoggingAndParameters();
myInstance.myMethod('Hello', 42); // Output: myMethod execution: 0.567ms
myInstance.anotherMethod(true); // No output (log level is 0 for this method)

Common Mistakes

  1. Forgetting to apply the decorator to a class or method: Make sure to add the @decoratorName annotation before the class or method declaration.
  2. Incorrect usage of parameterized decorators: Ensure that you pass the correct number and type of parameters to the decorator function.
  3. Misunderstanding the scope of the decorator: Remember that a decorator has access to the original class properties and methods through the this keyword.
  4. Not properly modifying the property descriptor: Make sure to modify the value, writable, enumerable, or configurable properties of the property descriptor as needed.
  5. Ignoring the return value of the decorator function: The decorator function should return a new constructor or a modified version of the original class.
  6. Using decorators with ES5-style classes: Decorators are a TypeScript feature and cannot be used with ES5-style classes. You can use a transpiler like Babel to convert TypeScript code back to ES5 syntax if needed.
  7. Forgetting to call the original method using originalMethod.apply(this, args): Make sure to call the original method using the modified property descriptor's value.
  8. Not handling errors properly in decorators: Decorators should handle any errors that may occur during their execution and propagate them appropriately.
  9. Overusing decorators: While decorators can be a powerful tool, it's essential to use them judiciously and avoid overcomplicating your codebase with unnecessary decorations.
  10. Not considering performance implications: Decorators can have some performance overhead due to their runtime nature. Be mindful of this when using decorators in critical parts of your application.

Practice Questions

  1. Create a decorator that logs all calls to a specific method and its execution time, but only for methods with more than 3 parameters.
  2. Create a parameterized decorator that enforces custom validation rules on class properties based on regular expressions.
  3. Modify the logMethodCalls decorator from the worked example to log only the first call to a method and the number of subsequent calls.
  4. Create a decorator that automatically injects dependencies into a class constructor using dependency injection containers like Angular's DI or InversifyJS.
  5. Write a decorator that ensures that all methods in a class have at least one parameter. If a method does not meet this requirement, throw an error.
  6. Create a decorator that logs the execution time of asynchronous methods using Promises.
  7. Implement a decorator that caches the results of expensive calculations to improve performance.
  8. Write a decorator that generates unit tests for a class's methods using a testing framework like Jest or Mocha.
  9. Create a decorator that automatically adds logging and error handling to all methods in a class using a third-party logging library like Winston or Bunyan.
  10. Implement a decorator that ensures that all properties in a class are initialized with default values if they are not explicitly set during instantiation.

FAQ

Q: Can I use decorators with ES5-style classes?

A: No, decorators are a TypeScript feature and cannot be used with ES5-style classes. You can use a transpiler like Babel to convert TypeScript code back to ES5 syntax if needed.

Q: How do I create a decorator for a class constructor?

A: To create a decorator for a class constructor, you should pass the constructor function as the first argument to the decorator function instead of the class itself.

Q: Can I use decorators with private or protected properties and methods?

A: Yes, decorators have access to private and protected properties and methods through the this keyword. However, you should be careful not to modify these properties and methods directly in the decorator function.

Q: How can I create a reusable decorator that works with multiple classes?

A: To create a reusable decorator, you should define it as a separate function and then apply it to multiple classes using the @decorator

TS Decorators (Java) | Java | XQA Learn