Understanding and Using TypeScript Decorators [Practical Examples]

While working on a TypeScript project, I needed to add extra information (metadata) to classes and their properties, such as logging when a method runs or validating input, without modifying their actual implementation.

That’s when I discovered TypeScript decorators. Decorators allow you to add metadata, modify, or extend the behavior of classes, methods, and properties in a clean and reusable way.

In this article, I’ll cover everything you need to know about TypeScript decorators, from basic usage to advanced techniques. So let’s dive in!

What are TypeScript Decorators?

Decorators are a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. They use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

Decorators allow you to add metadata, modify, or extend the behavior of classes, methods, and properties in a clean and reusable way. Instead of adding repetitive code everywhere, we could use decorators to handle logging, validation, or adding metadata in a single place while keeping the main logic clean.

Decorators are an experimental feature in TypeScript, so you’ll need to enable them in your tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

Types of Decorators in TypeScript

Class Decorators in TypeScript

Class decorators are applied to the constructor of the class and can be used to observe, modify, or replace a class definition.

Here’s a simple example of a class decorator:

function Logger(target: Function) {
  console.log(`Creating instance of: ${target.name}`);
}
@Logger
class User {
  constructor(public name: string) {}
}
// When you create a new User instance, you'll see the log
const user = new User("John");
Understanding and Using TypeScript Decorators

Method Decorators in TypeScript

Method decorators are declared just before a method declaration and can be used to observe, modify, or replace a method definition.

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with`, args);
    return original.apply(this, args);
  };

  return descriptor;
}

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

const calc = new Calculator();
calc.add(5, 3); // Logs: "Calling add with [5, 3]" and returns 8
Method Decorators in TypeScript

Property Decorators in TypeScript

Property decorators are declared just before a property declaration and can be used to observe, modify, or replace a property definition.

function Format(formatString: string) {
  return function (target: any, propertyKey: string) {
    let value: string;

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

    const setter = function (newVal: string) {
      value = formatString.replace("%s", newVal);
    };

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

class Greeting {
  @Format("Hello, %s!")
  message!: string;
}

const greeting = new Greeting();
greeting.message = "John";
console.log(greeting.message); // Outputs: "Hello, John!"
Property Decorators in TypeScript

Real-World Use Cases for Decorators in TypeScript

Creating an API Route Decorator in TypeScript

Here’s how you might create a decorator for defining API routes in a Node.js application with Express:

import express, { Request, Response } from 'express';
const app = express();

// Decorator factory
function Route(path: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;

    app.get(path, async (req: Request, res: Response) => {
      try {
        // Check if the method expects parameters
        const result = original.length >= 1
          ? await original.call(target, req, res)
          : await original.call(target);

        res.json(result);
      } catch (error) {
        if (error instanceof Error) {
          res.status(500).json({ error: error.message });
        } else {
          res.status(500).json({ error: "An unknown error occurred." });
        }
      }
    });

    return descriptor;
  };
}

class UserController {
  @Route('/api/users')
  async getUsers() {
    // Fetch users from database (mock)
    return [{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' }];
  }

  @Route('/api/users/:id')
  async getUser(req: Request) {
    const userId = req.params.id;
    // Fetch user from database (mock)
    return { id: userId, name: 'John Doe' };
  }
}

new UserController();

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
Creating an API Route Decorator in TypeScript
API Route Decorator in TypeScript

Creating a Property Validation Decorator in TypeScript

Here’s a practical example of property validation with decorators:

function MinLength(min: number) {
  return function (target: any, propertyKey: string) {
    let value: string;

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

    const setter = function (newVal: string) {
      if (newVal.length < min) {
        throw new Error(`${propertyKey} should be at least ${min} characters`);
      }
      value = newVal;
    };

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

class Registration {
  @MinLength(8)
  password!: string;

  constructor(password: string) {
    this.password = password;
  }
}

try {
  const registration = new Registration("pass");
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message); // Outputs: "password should be at least 8 characters"
  } else {
    console.error("An unknown error occurred.");
  }
}
Creating a Property Validation Decorator in TypeScript

Decorator Composition in TypeScript

You can apply multiple decorators to a declaration. They are evaluated in the order they appear in the code (from bottom to top):

function First() {
  console.log("First decorator");
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("First decorator called");
  };
}

function Second() {
  console.log("Second decorator");
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Second decorator called");
  };
}

class Example {
  @First()
  @Second()
  method() {}
}

// Output:
// "First decorator"
// "Second decorator"
// "Second decorator called"
// "First decorator called"
Decorator Composition in TypeScript

Conclusion

I hope you found this article helpful. TypeScript decorators are an incredibly powerful feature that can help you write cleaner, more maintainable code. They’re especially useful for cross-cutting concerns, such as logging, validation, and authorization.

Although they’re still technically an experimental feature, they’ve been around for a long time and are widely used in popular frameworks such as Angular, NestJS, and TypeORM.

Other TypeScript articles you may also like:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.