TypeScript Decorators

A decorator is a function that gets attached to a class or a part of a class (like a method or property) to modify or add behavior to it.

You decorate something by placing the @ symbol followed by the decorator function name above a class or class member.

Here's a quick example of a decorator. You can read the rest of the tutorial to learn more.

Example

// Decorator function that logs when a class is created
function logger(value: Function, context: ClassDecoratorContext) {

    // Print the name of the class being decorated
    console.log(`Creating class - ${context.name}`)
}

// Here, @logger is a decorator that calls the logger() function
@logger
class Person {
    constructor(public name: string) {}
}

// Output: Creating class - Person

Here, logger() is a decorator function that modifies an entire class. Using @logger before the class declaration ensures that the decorator runs as soon as the class is defined, not when it's instantiated.


Enabling Decorators

To use decorators, you need to add the following settings in your TypeScript configuration file, i.e., tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": true
  }
}

Types of Decorators

The main types of decorators available in TypeScript are:

Decorator Type Applies To Use Cases
Class Whole class Logging, sealing
Property Class fields Tracking access, validation
Method Class methods Timing, logging calls
Accessor Getters/setters Hiding, modifying behavior

Tip: If you're still confused about decorators, just think of them as a "sticker" you "stick" to classes or their members to provide extra information or functionality.


1. Class Decorators

We use class decorators to modify a class.

  • What it does: Applies to the entire class.
  • What it gets: The class constructor.
  • Use case: Can modify or seal the class.

Syntax

function decoratorName(value: Function, context: ClassDecoratorContext) {
    // Decorator code
}

Here's what the parameters of a class decorator signify:

Parameter Type Description
value Function Refers to the class constructor being decorated.
context ClassDecoratorContext Gives metadata about the decorated class (such as the class name, its kind, and addInitializer).

Understanding Decorators

Let's try to understand the basic working of decorators using our previous example:

// Create a decorator function
// that logs when a class is created
function logger(value: Function, context: ClassDecoratorContext) {

    // Print the name of the class being decorated
    console.log(`Decorated Entity: ${context.name}`)

    // Print the kind of the decorated entity
    console.log(`Entity Kind: ${context.kind}`)

    // Print the raw value of the constructor function
    console.log("Entity Constructor:")
    console.log(value)
}

// Use the decorator on a class
@logger
class Person {
    constructor(public name: string) {}
}

Output

Decorated Entity: Person
Entity Kind: class
Entity Constructor:
[class Person]

Here, the decorator function logger() is a class decorator that modifies a class.

Inside the decorator,

  • context.name - Gives the name of the entity (class) we'll decorate.
  • context.kind - Gives the kind of the entity we'll decorate.
  • console.log(value) - Prints the raw value of the constructor function.

Finally, we decorate a class by using the syntax @decoratorName before the class declaration:

// Use the decorator on a class
@logger
class Person {
    constructor(public name: string) {}
}

Notice that we haven't created an instance of the class. This shows that the decorator executes when the class is defined, not when it is instantiated.

Tip: You can use generics for better type safety. For example,

function logger<T extends Function>(value: T, context: ClassDecoratorContext) {
    // Decorator code
}

Example 1: Decorator to Seal a Class

Now, let's create a decorator to seal a class, i.e., prevent modifications to its structure, such as adding or removing properties.

function sealed<T extends Function>(value: T, context: ClassDecoratorContext) {
    Object.seal(value);
    Object.seal(value.prototype);
    console.log("Class sealed!");
}

@sealed
class Vehicle {
    wheels: number = 4;
}

// Try to add a new property
// Will fail silently or throw error in strict mode
(Vehicle as any).newProp = "test";
console.log((Vehicle as any).newProp);

Output

Class sealed!
undefined

2. Property Decorators

Property decorators are used to modify or extend the behavior of class fields.

  • What it does: Applies to a class field.
  • What it gets: Metadata via ClassFieldDecoratorContext.
  • Use case: Can log reads/writes or enforce rules.

Syntax

function decoratorName(value: undefined, context: ClassFieldDecoratorContext) {
    // Decorator code
}

You can ignore the value parameter for now — it's always undefined because the decorator is applied at class definition time, before any instances are created.


Example 2: TypeScript Property Decorator

function logAccess(value: undefined, context: ClassFieldDecoratorContext) {
    let backingField = Symbol();

    context.addInitializer(function () {
        Object.defineProperty(this, context.name, {
            get() {
                console.log(`Getting property: ${String(context.name)}`);
                return this[backingField];
            },
            set(newValue: any) {
                console.log(`Setting property: ${String(context.name)} to ${newValue}`);
                this[backingField] = newValue;
            },
            enumerable: true,
            configurable: true
        });
    });
}

class Product {
    @logAccess
    name: string;

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

const laptop = new Product("Laptop");
console.log(laptop.name); // Logs access
laptop.name = "Gaming Laptop"; // Logs change

Output

Setting property: name to Laptop
Getting property: name
Laptop
Setting property: name to Gaming Laptop

Here, logAccess() is a property decorator that logs access to a class property and sets the value of the property.

Inside this decorator,

  • backingField is a private, internal key used to store the actual value of the decorated property (name) without interfering with the public interface.
  • context.addInitializer() lets you set up the property using a custom getter and setter during instance initialization.

3. Method Decorator

We use method decorators to decorate the methods of a class.

  • What it does: Applies to a method.
  • What it gets: Metadata via ClassMethodDecoratorContext.
  • Use case: Add logic before/after method runs.

Syntax

function decoratorName(target: any, context: ClassMethodDecoratorContext) {
    // Decorator code
}

Here, target is the original method function being decorated.


Example 3: TypeScript Method Decorator

function measureTime(target: any, context: ClassMethodDecoratorContext) {
    const originalMethod = target;

    return function (this: any, ...args: any[]) {
        console.time("Execution");
        const result = originalMethod.apply(this, args);
        console.timeEnd("Execution");
        return result;
    };
}

class Calculator {
    @measureTime
    add(a: number, b: number): number {
        // Simulate delay
        for (let i = 0; i < 1000000; i++) {}
        return a + b;
    }
}

const calc = new Calculator();
calc.add(5, 3);

Output

Execution: 1.648ms

This program uses a decorator called measureTime() to measure how long the add() method takes to run.

The decorator wraps the add() method and logs the execution time using console.time() and console.timeEnd().

How It Works

When calc.add(5, 3) is called, the decorator:

  1. Starts a timer.
  2. Runs the original add() method.
  3. Stops the timer.
  4. Logs how long it took.

The for loop inside add() adds a short delay so we can see the timing.


4. Accessor Decorators

Accessor decorators are used to modify the behavior of getters and setters in a class.

  • What it does: Applies to get or set accessors.
  • What it gets: ClassGetterDecoratorContext or ClassSetterDecoratorContext.
  • Use case: Control visibility or behavior of accessors.

Syntax

function decoratorName(
    value: any,
    context: ClassGetterDecoratorContext | ClassSetterDecoratorContext
) {
    // Decorator code
}

Here, value is the actual getter or setter function. Because we've used a union type for context, the decorator can work for both getters and setters.


Example 4: TypeScript Accessor Decorators

function isHidden(
    value: any,
    context: ClassGetterDecoratorContext | ClassSetterDecoratorContext
) {
    context.addInitializer(function () {
        Object.defineProperty(this, context.name, {
            enumerable: false
        });
    });
    return value;
}

class UserProfile {
    private _age: number;

    constructor(age: number) {
        this._age = age;
    }

    @isHidden
    get age(): number {
        return this._age;
    }

    set age(value: number) {
        this._age = value;
    }
}

const user = new UserProfile(30);

// Using the getter gives 'undefined' because it is hidden
console.log(user.age);

// Verifying that '_age' property still exists
console.log(Object.keys(user));

Output

undefined
[ '_age' ]

Here, the isHidden() decorator hides the accessor from object enumeration. In our case, we have hidden get age() from enumeration.

Thus, you won't be able to view the _age property since its getter function is hidden.

Did you find this article helpful?

Our premium learning platform, created with over a decade of experience and thousands of feedbacks.

Learn and improve your coding skills like never before.

Try Programiz PRO
  • Interactive Courses
  • Certificates
  • AI Help
  • 2000+ Challenges