TypeScript decorators let you attach behavior to classes, methods, accessors, properties, and parameters without modifying the original code directly. They're most visible in Angular, but the pattern is useful anywhere you want to keep cross-cutting concerns like logging, validation, or metadata out of your core business logic. This post covers how decorators work, the five types TypeScript supports, and how to build your own.
Understanding the Fundamentals of TypeScript Decorators
A decorator is a function. When you put @Something above a class or method, TypeScript calls that function with metadata about the target: the constructor, the method descriptor, the property name, and so on. The decorator can wrap behavior, attach new properties, or record metadata for later use.
Why Decorators Matter
Decorators solve a specific problem: cross-cutting concerns accumulate in methods over time. Logging, metrics, validation, and permission checks end up scattered inside functions that should only care about one thing. Decorators let you pull that logic out and reuse it across the codebase with a single annotation.
Once you've written a logging decorator, you can apply it to a hundred methods without touching any of them. When the logging behavior needs to change, you change it in one place. That's the real value, not the syntax.
Angular uses decorators heavily for @Component, @Injectable, @Input, and similar markers. Understanding what's happening under the hood helps when you need to debug, build custom library extensions, or write framework-agnostic code.
Types of Decorators in TypeScript
TypeScript supports five decorator types, each targeting a different language construct.
Class decorators take a constructor as their argument. They can wrap the constructor, attach new properties to the prototype, or replace the class entirely. The most common use is marking classes with roles or performing initialization logic.
Method decorators receive the class prototype, the method name, and the property descriptor. You replace descriptor.value with a wrapper function. This is the most frequently used type, good for logging, caching, and access control.
Accessor decorators work the same way as method decorators but target getters and setters. Useful for validation or transformation on property access, though less commonly needed than method decorators.
Property decorators receive the target and the property name. They cannot modify property initializers directly, but they can store metadata via reflect-metadata. ORMs use this pattern extensively to map class properties to database columns.
Parameter decorators annotate method parameters with their index in the signature. Dependency injection frameworks use this to know which arguments to inject automatically.
Prerequisites for Using Decorators
Enable two compiler options in your tsconfig.json:
{
"compilerOptions": {
"target": "ES6",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
...
},
...
}
experimentalDecorators enables the decorator syntax. emitDecoratorMetadata emits type information at runtime, which the reflect-metadata library uses for advanced reflection scenarios like dependency injection.
How to Use Decorators in Practice
Class Decorator Example
Here is a simple class decorator that adds a timestamp property to track instantiation times:
function Timestamped(constructor: Function) {
constructor.prototype.createdAt = new Date();
}
@Timestamped
class ReportService {
constructor() {
// Some initialization code
}
generateReport() {
console.log("Report generated");
}
}
const service = new ReportService();
console.log(service["createdAt"]); // A Date object representing creation time
Timestamped modifies the constructor's prototype, so every instance of ReportService gets a createdAt property set to the time the class was first decorated. Note that this runs once at class definition time, not on each instantiation, which is a common gotcha.
Method Decorator Example
Method decorators allow you to intercept and augment function calls. Here's a simple logging decorator:
function LogMethod(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyName} with arguments:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result of ${propertyName}:`, result);
return result;
};
return descriptor;
}
class TaskService {
@LogMethod
runTask(taskId: string) {
return `Task ${taskId} completed successfully!`;
}
}
const taskService = new TaskService();
taskService.runTask("1234");
LogMethod replaces descriptor.value with a wrapper that logs before and after the original call. The original method runs via apply(this, args) to preserve the correct this binding.
Property Decorator Example
Property decorators can attach metadata to specific class properties. For instance, imagine an ORM that needs to identify which properties map to database columns:
import "reflect-metadata";
const COLUMN_KEY = Symbol("column");
/**
* Attach metadata to a property to mark it as a DB column.
*/
function Column(columnName?: string): PropertyDecorator {
return (target, propertyKey) => {
Reflect.defineMetadata(COLUMN_KEY, columnName || propertyKey, target, propertyKey);
};
}
class User {
@Column("user_id")
id: number;
@Column()
username: string;
@Column()
email: string;
}
// Later you can retrieve metadata to build queries or define migrations
function getColumnMetadata(target: any, propertyKey: string) {
return Reflect.getMetadata(COLUMN_KEY, target, propertyKey);
}
Each decorated property stores metadata about its column name. The ORM layer reads that metadata at runtime to construct queries, without any of this logic living inside the User class itself.
How to Create Your Own Decorators
Building a custom decorator means writing a function that matches the expected signature for the target type. The validator example below combines a parameter decorator and a method decorator to enforce non-empty arguments.
Step-by-Step Example: Custom Validator Decorator
- Create the decorator function:
import "reflect-metadata";
const VALIDATE_KEY = Symbol("validateParams");
function NotEmpty(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
VALIDATE_KEY,
existingRequiredParameters,
target,
propertyKey
);
}
- Create a helper function to execute validations before calling the original method:
function Validate(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] =
Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyName) || [];
requiredParameters.forEach((index) => {
if (args[index] === null || args[index] === undefined || args[index] === "") {
throw new Error(`Validation Error: Parameter at index ${index} is empty`);
}
});
return method.apply(this, args);
};
return descriptor;
}
- Apply these decorators in a class:
class MailService {
@Validate
sendEmail(@NotEmpty recipient: string, message: string) {
console.log(`Sending email to ${recipient} with message: ${message}`);
}
}
const mailService = new MailService();
mailService.sendEmail("john@example.com", "Hello John!"); // Works fine
mailService.sendEmail("", "This should fail validation"); // Throws Validation Error
@NotEmpty records which parameter indexes require validation. @Validate reads those indexes before invoking the original method and throws if any of them are empty. The validation logic lives entirely in the decorators, not in sendEmail.
Decorators in Angular
Angular uses class decorators to mark components, services, directives, and pipes. When you write @Component(...), Angular's compiler reads the metadata from that decorator to understand how to compile and wire up the class. @Injectable() tells the dependency injection system that the class can be instantiated and injected elsewhere.
You don't need to understand the internals to use Angular, but knowing how decorators work makes debugging much less mysterious when metadata is missing or misconfigured.
Best Practices for Decorators
Keep each decorator focused on one responsibility. A decorator that logs, validates, and transforms data is harder to debug than three separate ones. When decorators modify prototypes or descriptors, document what they change so future readers don't have to reverse-engineer it. The reflect-metadata library is powerful but can get hard to reason about if overused. If you're attaching metadata for a single use case, consider whether a simpler approach would work.
Test your decorators explicitly. Because they apply cross-cutting behavior, they're easy to overlook in unit tests. Write tests that verify the decorator's behavior in isolation, not just the method it wraps.
Moving Forward with TypeScript Decorators
The pattern scales well. Decorator factories let you pass configuration arguments to a decorator. Stacking multiple decorators on a single method is straightforward, though execution order (bottom to top) trips people up at first. For larger projects, decorators pair well with dependency injection and middleware patterns.
The main thing to internalize is that a decorator is just a function. Once that clicks, the rest is mechanics.