Skip to content

TypeScript Decorators — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about TypeScript Decorators. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript decorators are special declarations that attach to classes, methods, properties, accessors, and parameters to modify their behavior at design time — enabling metaprogramming patterns like logging, validation, authorization, and dependency injection.

What You'll Learn

  • Class decorators for extending constructors
  • Method decorators for wrapping behavior
  • Property and accessor decorators
  • Parameter decorators
  • Metadata reflection with reflect-metadata

Why It Matters

Decorators are the foundation of Angular and NestJS — two of the most popular TypeScript frameworks. Understanding decorators unlocks the ability to write cross-cutting concerns (logging, caching, validation) as reusable annotations rather than repetitive boilerplate.

Real-World Use

NestJS (used by DodaTech for internal microservices) uses decorators extensively: @Controller(), @Get(), @Body(), @Injectable(). Each decorator adds metadata that NestJS's runtime uses to wire up routing, dependency injection, and request handling.

Learning Path

flowchart LR
  A[Inheritance] --> B[Decorators]
  B --> C[This Typing]
  B --> D[You Are Here]
  C --> E[Index Signatures]
  E --> F[Type Guards]

Enabling Decorators

Decorators are an experimental feature in TypeScript. Enable in tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Class Decorators

A class decorator receives the constructor and can modify or replace it:

function LogClass<T extends { new (...args: any[]): object }>(target: T): T {
  console.log(`Class defined: ${target.name}`);
  return target;
}

@LogClass
class UserService {
  constructor() {
    console.log("UserService instance created");
  }

  getUsers(): string[] {
    return ["Alice", "Bob"];
  }
}
// Output when class is defined:
// Class defined: UserService

const service = new UserService();
// UserService instance created

Modifying the Constructor

function Singleton<T extends { new (...args: any[]): object }>(target: T): T {
  let instance: T;

  const modified = class extends target {
    constructor(...args: any[]) {
      super(...args);
      if (instance) {
        return instance;
      }
      instance = this as unknown as T;
    }
  } as unknown as T;

  return modified;
}

@Singleton
class DatabaseConnection {
  connect(): void {
    console.log("Connected to database");
  }
}

const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true — same instance

Method Decorators

A method decorator intercepts property descriptor and can wrap the original method:

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

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

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

  @Log
  multiply(a: number, b: number): number {
    return a * b;
  }
}

const calc = new Calculator();
calc.add(3, 4);    // [LOG] Calling add with args: [3, 4]
                   // [LOG] add returned: 7
calc.multiply(2, 5); // [LOG] Calling multiply...

Async Method Decorator

function Time(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    const start = Date.now();
    const result = await originalMethod.apply(this, args);
    const duration = Date.now() - start;
    console.log(`[PERF] ${propertyKey} took ${duration}ms`);
    return result;
  };
}

class ScanService {
  @Time
  async scanFile(path: string): Promise<string> {
    // Simulate async scan
    await new Promise(resolve => setTimeout(resolve, 100));
    return `Scan of ${path} complete`;
  }
}

Authorization Decorator

function Authorized(role: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      // In real app, get user from request context
      const userRole = "admin";

      if (userRole !== role && userRole !== "admin") {
        throw new Error(`Unauthorized: ${role} role required`);
      }

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

class AdminAPI {
  @Authorized("admin")
  deleteUser(userId: string): void {
    console.log(`Deleted user ${userId}`);
  }

  @Authorized("user")
  viewProfile(userId: string): void {
    console.log(`Viewing profile ${userId}`);
  }
}

Property Decorators

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

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

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

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

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

const greet = new Greeting();
greet.message = "Alice";
console.log(greet.message); // Hello, Alice!

Parameter Decorators

import "reflect-metadata";

function Inject(serviceName: string): ParameterDecorator {
  return (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) => {
    const existingParams: string[] = Reflect.getOwnMetadata("inject:params", target, propertyKey!) || [];
    existingParams[parameterIndex] = serviceName;
    Reflect.defineMetadata("inject:params", existingParams, target, propertyKey!);
  };
}

class UserController {
  private userService: any;

  constructor(@Inject("UserService") userService: any) {
    this.userService = userService;
    const params: string[] = Reflect.getOwnMetadata("inject:params", this, "constructor");
    console.log("Injected:", params); // Injected: ["UserService"]
  }
}

new UserController({});

Decorator Factories

A decorator factory is a function that returns a decorator, allowing parameterization:

function Log(prefix: string = "LOG") {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
    const originalMethod = descriptor.value;

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

class Service {
  @Log("TRACE")
  doSomething(): void {
    console.log("Doing something");
  }

  @Log("PERF")
  doSomethingElse(): void {
    console.log("Doing something else");
  }
}

Common Mistakes

1. Forgetting to Enable experimentalDecorators

Without this flag, decorators cause compile errors.

2. Using Decorators in Runtime-Heavy Ways

Decorators execute when the class is defined (not instantiated). Avoid heavy computation in decorator factories.

3. Not Handling this Context in Method Decorators

Use function() (not arrow function) in the wrapped method to preserve this:

// Arrow function — loses this context
descriptor.value = (...args: any[]) => originalMethod.apply(this, args);

4. Decorator Order Confusion

Decorators apply in reverse order of their proximity to the target:

@DecoratorA
@DecoratorB
class MyClass {} // DecoratorB runs on the class, then DecoratorA

5. Relying on Decorators for Runtime Type Safety

Decorators can enhance type safety but don't replace proper TypeScript types.

Practice Questions

  1. What is a decorator factory? A function that returns a decorator, allowing configuration via parameters.

  2. What can decorators be applied to? Classes, methods, properties, accessors (get/set), and parameters.

  3. What is the purpose of emitDecoratorMetadata? It enables TypeScript to emit type metadata that can be read at runtime via reflect-metadata.

  4. How do method decorators work? They receive the target Prototype, method name, and property descriptor, allowing them to wrap or replace the original method.

Challenge: Write a @Throttle(ms: number) method decorator that prevents a method from being called more than once every ms milliseconds. Use it on a handleClick() method.

FAQ

Can I use decorators without the `experimentalDecorators` flag?

No. Decorators are an experimental TC39 proposal. The flag is required until decorators reach Stage 4 and are natively supported.

Do decorators work with arrow function properties?

No. Decorators attach to prototype methods or class constructors, not instance-specific arrow function properties.

What is `reflect-metadata`?

A Polyfill for the Reflect API that enables reading type information emitted by emitDecoratorMetadata. Required by Angular and many decorator-based frameworks.

Are decorators part of JavaScript?

Decorators are a Stage 3 TC39 proposal. They may become part of JavaScript in the future, with some syntax differences from TypeScript's current implementation.

Can I use decorators in production?

Yes. Angular and NestJS use them in production. Just be aware they're still experimental and syntax may change.

Mini Project: Middleware System

// src/middleware.ts

function Middleware(...fns: Array<(ctx: any, next: () => void) => void>) {
  return function <T extends { new (...args: any[]): object }>(target: T): T {
    return class extends target {
      constructor(...args: any[]) {
        super(...args);
      }

      executeMethod(method: string, ...methodArgs: any[]): any {
        let index = 0;
        const runMiddleware = () => {
          if (index < fns.length) {
            const fn = fns[index++];
            fn({ method, args: methodArgs }, runMiddleware);
          }
        };
        runMiddleware();
        return (this as any)[method](...methodArgs);
      }
    } as unknown as T;
  };
}

function Logging(ctx: any, next: () => void): void {
  console.log(`[Middleware] ${ctx.method} called with:`, ctx.args);
  next();
}

function Validation(ctx: any, next: () => void): void {
  if (ctx.method === "createUser" && (!ctx.args[0]?.name)) {
    throw new Error("Validation failed: name is required");
  }
  next();
}

@Middleware(Logging, Validation)
class UserAPI {
  createUser(data: { name: string }): string {
    console.log(`Creating user: ${data.name}`);
    return `User ${data.name} created`;
  }

  deleteUser(id: string): string {
    console.log(`Deleting user: ${id}`);
    return `User ${id} deleted`;
  }
}

const api = new UserAPI();
console.log(api.createUser({ name: "Alice" }));
// [Middleware] createUser called with: [{ name: "Alice" }]
// Creating user: Alice
// User Alice created

What's Next

Now explore this typing for safer context binding:

Lesson Description
{{< ref "/programming-languages/typescript/21-inheritance" >}} Review inheritance
{{< ref "/programming-languages/typescript/23-this-typing" >}} This parameter and type
{{< ref "/programming-languages/typescript/24-index-signatures" >}} Index signatures and Record utility

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro