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:
- Starts a timer.
- Runs the original
add()
method. - Stops the timer.
- 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
orset
accessors. - What it gets:
ClassGetterDecoratorContext
orClassSetterDecoratorContext
. - 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.