TypeScript Decorators

A decorator is a special function that attaches extra behavior to a class, method, property, or parameter without modifying the original code. Decorators use the @ symbol followed by the decorator name. They run at the time a class is defined — not when objects are created — and work like wrappers that observe or modify the target they decorate.

Enabling Decorators

Decorators require the experimentalDecorators flag enabled in tsconfig.json:

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

How Decorators Work

  Without decorator:               With decorator:
  +------------------+             +------------------+
  | class Product {  |     @Log    | class Product {  |
  |   getName() { }  |  ---------> |   getName() { }  |
  | }                |  wraps it   |   + logging       |
  +------------------+             |   + timing        |
                                   +------------------+
  Original class unchanged         Class gains new behavior

Class Decorators

A class decorator receives the constructor function of the class. It can observe, modify, or replace the class.

// Simple logging decorator
function LogClass(constructor: Function): void {
    console.log("Class created: " + constructor.name);
}

@LogClass
class UserService {
    createUser(name: string): void {
        console.log("Creating user: " + name);
    }
}

// Output on class definition: Class created: UserService

let service = new UserService();
service.createUser("Rekha"); // Creating user: Rekha

Class Decorator with Modification

// Decorator that adds a 'version' property to any class
function Versioned(version: string) {
    return function(constructor: Function): void {
        constructor.prototype.version = version;
    };
}

@Versioned("2.0.0")
class AppConfig {
    appName: string = "eStudy247";
}

let config = new AppConfig() as AppConfig & { version: string };
console.log(config.appName);  // eStudy247
console.log(config.version);  // 2.0.0

Method Decorators

A method decorator wraps a class method. It receives the target object, the method name, and the property descriptor. This is the most commonly used type of decorator.

// Decorator that logs method calls and execution time
function LogMethod(
    target: any,
    methodName: string,
    descriptor: PropertyDescriptor
): PropertyDescriptor {
    let originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
        console.log("Calling: " + methodName + "(" + args.join(", ") + ")");
        let start = Date.now();
        let result = originalMethod.apply(this, args);
        let elapsed = Date.now() - start;
        console.log(methodName + " completed in " + elapsed + "ms");
        return result;
    };

    return descriptor;
}

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

let calc = new Calculator();
let result = calc.add(10, 20);
// Calling: add(10, 20)
// add completed in 0ms
console.log(result); // 30

Property Decorators

A property decorator receives the target object and the property name. It can observe or transform property behavior.

// Decorator that marks a property as readonly at runtime
function ReadOnly(target: any, propertyKey: string): void {
    Object.defineProperty(target, propertyKey, {
        writable: false,
        enumerable: true,
        configurable: false
    });
}

class Config {
    @ReadOnly
    appName: string = "eStudy247";

    version: string = "1.0";
}

let cfg = new Config();
console.log(cfg.appName); // eStudy247
cfg.appName = "Other";    // Fails silently (or throws in strict mode)
cfg.version = "2.0";      // Works normally

Parameter Decorators

// Decorator that logs which parameter index was decorated
function LogParam(target: any, methodName: string, paramIndex: number): void {
    console.log("Decorated parameter " + paramIndex + " in method: " + methodName);
}

class NotificationService {
    send(@LogParam message: string, recipient: string): void {
        console.log("Sending '" + message + "' to " + recipient);
    }
}
// Output when class loads: Decorated parameter 0 in method: send

Accessor Decorators

An accessor decorator applies to get or set accessors of a class property. It modifies how getting or setting the value works.

function Validate(target: any, name: string, descriptor: PropertyDescriptor): PropertyDescriptor {
    let originalSet = descriptor.set!;

    descriptor.set = function(value: number) {
        if (value < 0) {
            throw new Error(name + " cannot be negative.");
        }
        originalSet.call(this, value);
    };

    return descriptor;
}

class Product {
    private _price: number = 0;

    @Validate
    set price(value: number) {
        this._price = value;
    }

    get price(): number {
        return this._price;
    }
}

let p = new Product();
p.price = 500;
console.log(p.price);  // 500

p.price = -10; // Error: price cannot be negative.

Decorator Factories

A decorator factory is a function that returns a decorator. This allows decorators to accept custom arguments.

// Factory that accepts a role name
function RequireRole(role: string) {
    return function(target: any, methodName: string, descriptor: PropertyDescriptor): PropertyDescriptor {
        let original = descriptor.value;

        descriptor.value = function(this: any, ...args: any[]) {
            if (this.userRole !== role) {
                throw new Error("Access denied. Requires role: " + role);
            }
            return original.apply(this, args);
        };

        return descriptor;
    };
}

class AdminPanel {
    userRole: string = "admin";

    @RequireRole("admin")
    deleteUser(userId: number): void {
        console.log("User " + userId + " deleted.");
    }
}

let panel = new AdminPanel();
panel.deleteUser(5);  // User 5 deleted.

panel.userRole = "viewer";
panel.deleteUser(5);  // Error: Access denied. Requires role: admin

Decorator Execution Order

  Multiple decorators on one target — execute bottom to top:

  @First       ← executes second
  @Second      ← executes first
  class MyClass { }

  Method decorator evaluation:
  1. Parameter decorators (innermost first)
  2. Method decorators
  3. Accessor decorators
  4. Property decorators
  5. Class decorators (last)

Decorator Types Summary

Decorator TypePlacementReceivesCommon Use
ClassAbove classConstructorLogging, versioning, metadata
MethodAbove methodTarget, name, descriptorLogging, caching, auth checks
PropertyAbove propertyTarget, nameValidation, readonly enforcement
AccessorAbove get/setTarget, name, descriptorValidation on set, formatting on get
ParameterBefore parameterTarget, method name, indexDependency injection, logging

Practical Example

// Memoization decorator — caches function results
function Memoize(target: any, name: string, descriptor: PropertyDescriptor): PropertyDescriptor {
    let cache = new Map<string, any>();
    let original = descriptor.value;

    descriptor.value = function(...args: any[]) {
        let key = JSON.stringify(args);
        if (cache.has(key)) {
            console.log("Cache hit for: " + name + "(" + key + ")");
            return cache.get(key);
        }
        let result = original.apply(this, args);
        cache.set(key, result);
        return result;
    };

    return descriptor;
}

class MathService {
    @Memoize
    fibonacci(n: number): number {
        if (n <= 1) return n;
        return this.fibonacci(n - 1) + this.fibonacci(n - 2);
    }
}

let ms = new MathService();
console.log(ms.fibonacci(10)); // 55
console.log(ms.fibonacci(10)); // Cache hit — 55 (instant)
console.log(ms.fibonacci(8));  // 21

Leave a Comment