Skip to content

TypeScript Decorators: Adding Metadata and Enhancing Functionality

TypeScript Decorators: Adding Metadata and Enhancing Functionality

Decorators in TypeScript provide a powerful way to add metadata, modify behavior, and enhance functionality in classes, properties, methods, and parameters. Decorators are widely used in frameworks like Angular to simplify configuration and keep code modular. In this guide, we’ll explore what decorators are, how to use them, and see practical examples that can help you build reusable, scalable applications in TypeScript.


What are TypeScript Decorators?

Decorators are special functions in TypeScript that allow you to add annotations or metadata to classes, methods, properties, and parameters. They’re similar to attributes in other languages and are evaluated at runtime. Decorators are currently an experimental feature in TypeScript, so they need to be enabled in the TypeScript configuration.

Enabling Decorators in TypeScript

To use decorators, you need to enable them in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Types of Decorators in TypeScript

TypeScript supports four types of decorators:

  1. Class Decorators: Applied to classes to add metadata or modify the class behavior.
  2. Method Decorators: Applied to methods within classes, useful for logging, validation, or modifying method functionality.
  3. Property Decorators: Applied to class properties, typically used to add metadata.
  4. Parameter Decorators: Applied to parameters within methods, often used for dependency injection.

Class Decorators

Class decorators are functions that take a constructor as an argument and allow you to enhance or modify the class. They’re useful for logging, enforcing certain rules, or adding metadata.

Example: Adding Metadata with Class Decorators

// @filename: index.ts
function Entity(constructor: Function) {
  console.log(`Entity: ${constructor.name} has been created`)
}

@Entity
class User {
  constructor(public name: string) {}
}

const user = new User('Alice') // Output: "Entity: User has been created"

In this example, Entity is a class decorator that logs a message when the class is created. The decorator adds behavior without modifying the class’s actual code.

Example: Extending Class Functionality with Decorators

// @filename: index.ts
function WithTimestamp<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    timestamp = new Date()
  }
}

@WithTimestamp
class Order {
  constructor(public productId: number) {}
}

const order = new Order(101)
console.log(order.timestamp) // Outputs the current date and time

The WithTimestamp decorator adds a timestamp property to the Order class, extending its functionality in a reusable way.


Method Decorators

Method decorators are applied to class methods, allowing you to intercept method calls, log actions, or modify the return value. They’re ideal for adding cross-cutting concerns like logging, caching, or access control.

Example: Logging Method Calls

// @filename: index.ts
function Log(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyName} with args: ${args.join(', ')}`)
    return method.apply(this, args)
  }
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b
  }
}

const calculator = new Calculator()
console.log(calculator.add(2, 3)) // Logs: "Calling add with args: 2, 3" and Output: 5

The Log decorator wraps the add method, logging its arguments before executing it. This helps track method calls without modifying the method itself.

Example: Validating Method Input

You can use decorators to add input validation to methods, ensuring that arguments meet certain conditions.

// @filename: index.ts
function Positive(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value

  descriptor.value = function (...args: any[]) {
    if (args.some((arg) => arg <= 0)) {
      throw new Error(`${propertyName} requires positive numbers`)
    }
    return method.apply(this, args)
  }
}

class MathOperations {
  @Positive
  multiply(a: number, b: number): number {
    return a * b
  }
}

const math = new MathOperations()
console.log(math.multiply(5, 3)) // Output: 15
// console.log(math.multiply(-1, 3)); // Throws error: "multiply requires positive numbers"

The Positive decorator enforces that all arguments to multiply must be positive, throwing an error otherwise.


Property Decorators

Property decorators are applied to class properties and are often used to add metadata or track changes to properties.

Example: Adding Metadata to Properties

// @filename: index.ts
function ReadOnly(target: any, propertyName: string) {
  Object.defineProperty(target, propertyName, {
    writable: false,
  })
}

class Book {
  @ReadOnly
  title = 'TypeScript Essentials'
}

const book = new Book()
book.title = 'New Title' // Error: Cannot assign to read-only property 'title'

The ReadOnly decorator makes the title property immutable, enforcing that it cannot be modified after it’s set.

Example: Tracking Property Changes

// @filename: index.ts
function Track(target: any, propertyName: string) {
  let value = target[propertyName]

  Object.defineProperty(target, propertyName, {
    get: () => value,
    set: (newValue) => {
      console.log(`Setting ${propertyName} to ${newValue}`)
      value = newValue
    },
  })
}

class Settings {
  @Track
  theme = 'dark'
}

const settings = new Settings()
settings.theme = 'light' // Logs: "Setting theme to light"

In this example, Track logs every change to the theme property, providing a simple way to observe property modifications.


Parameter Decorators

Parameter decorators are applied to specific parameters in method definitions. They’re commonly used in frameworks for dependency injection, enabling more modular and testable code.

Example: Using Parameter Decorators for Metadata

// @filename: index.ts
function Required(target: any, propertyName: string, parameterIndex: number) {
  console.log(
    `Parameter at index ${parameterIndex} in ${propertyName} is required`
  )
}

class UserService {
  createUser(@Required name: string, @Required age: number) {
    console.log(`Creating user: ${name}, ${age}`)
  }
}

const service = new UserService()
service.createUser('Alice', 30) // Logs metadata for required parameters

The Required decorator adds metadata to specific parameters, which could later be used to enforce rules, validate input, or inject dependencies.


Practical Applications of Decorators

Decorators offer powerful ways to create clean and modular code, especially in larger applications. Here are some practical uses:

1. Logging and Debugging

By decorating methods with logging functions, you can track when and how often methods are called, simplifying debugging.

2. Input Validation

Method decorators are excellent for implementing validation rules on method inputs, ensuring that incorrect data doesn’t reach core logic.

3. Caching Results

You can use decorators to cache results of methods, especially for expensive or frequently called operations, improving performance.

4. Dependency Injection

Parameter decorators simplify dependency injection, providing a clean way to inject services or other dependencies into methods or constructors.

5. Access Control and Security

Decorators can enforce access control policies by restricting method calls to authorized users or roles, making it easier to secure your application.


Best Practices for Using Decorators

  1. Keep Decorators Modular: Make decorators reusable and avoid coupling them too tightly with specific methods or classes.
  2. Limit Side Effects: Avoid modifying the logic within methods directly; use decorators for logging, validation, or metadata instead.
  3. Test Decorators Independently: Test decorators separately from the methods they decorate to ensure they behave correctly.
  4. Document Decorator Usage: Clearly document any decorators used in your codebase, as they can change behavior in unexpected ways.
  5. Be Mindful of Performance: Some decorators, such as those for logging or caching, can impact performance, so use them judiciously.

Conclusion

TypeScript decorators are a powerful feature that can add functionality, enforce validation, and inject dependencies in a clean and modular way. By understanding the different types of decorators—class, method, property, and parameter decorators—you can create reusable, scalable, and maintainable code.

Whether you’re enhancing methods with logging, enforcing validation rules, or implementing caching, decorators provide a flexible approach to adding functionality in TypeScript applications. Start using decorators to simplify your code and enhance your TypeScript projects with clean, modular functionality.

TypeScript JavaScript Type Safety Angular Frontend Scalability
Share:

Continue Reading

TypeScript Namespaces and Modules: Organizing Large Codebases

As TypeScript projects grow, organizing code in a scalable and maintainable way becomes essential. Namespaces and modules are two techniques that help you structure code, manage dependencies, and prevent naming conflicts in large TypeScript codebases. In this guide, we will explore the differences between namespaces and modules, when to use each, and practical examples for organizing code in a TypeScript project.

Read article
TypeScriptJavaScriptType Safety

Advanced TypeScript Design Patterns for Enterprise Applications

Explore advanced TypeScript design patterns and architectural approaches for building scalable enterprise applications. Learn how to implement type-safe patterns like dependency injection, factory methods, decorators, and advanced generics. This comprehensive guide covers practical patterns for building maintainable and robust TypeScript applications.

Read article
TypeScriptJavaScriptType Safety

Getting Started with TypeScript: A Beginner\

JavaScript is a flexible, powerful language, but its dynamic typing can sometimes lead to unexpected bugs and runtime errors. TypeScript is a superset of JavaScript that introduces static typing, helping developers catch errors early in the development process and improving code quality. In this guide, we’ll explore the basics of TypeScript, including how to set up a project, use types, and leverage TypeScript’s powerful features to create reliable, scalable applications.

Read article
TypeScriptJavaScriptType Safety