Decorators are an experimental TypeScript feature that allows you to add metadata and modify classes, methods, properties, and parameters. They're heavily used in frameworks like Angular, NestJS, and TypeORM.
Note: Decorators are experimental. You must
enable them in tsconfig.json with
"experimentalDecorators": true
{
"compilerOptions": {
"target": "ES2020",
"experimentalDecorators": true,
"emitDecoratorMetadata": true // For metadata reflection
}
}
Class decorators are applied to the class constructor:
// Decorator factory function
function Component(config: { selector: string; template: string }) {
return function (constructor: Function) {
console.log(`Registering component: ${config.selector}`);
// Add metadata or modify the class
(constructor as any).metadata = config;
};
}
// Using the decorator
@Component({
selector: 'app-hero',
template: '<div>Hero Component</div>'
})
class HeroComponent {
name: string = "Hero";
}
// Decorator to make class immutable
function Frozen(constructor: Function) {
Object.freeze(constructor);
Object.freeze(constructor.prototype);
}
@Frozen
class ImmutableClass {
value: number = 42;
}
Method decorators can modify or observe method behavior:
// Log method calls
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned:`, result);
return result;
};
return descriptor;
}
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(5, 3);
// Logs: "Calling add with args: [5, 3]"
// Logs: "add returned: 8"
Property decorators add metadata to class properties:
// Mark property as required
function Required(target: any, propertyKey: string) {
let value: any;
const getter = () => {
return value;
};
const setter = (newValue: any) => {
if (newValue === null || newValue === undefined) {
throw new Error(`${propertyKey} is required!`);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@Required
username: string;
constructor(username: string) {
this.username = username;
}
}
// const user = new User(null); // Error: username is required!
Parameter decorators mark function parameters:
// Validate parameter
function Validate(
target: any,
propertyKey: string,
parameterIndex: number
) {
const existingParams = Reflect.getMetadata("validate", target, propertyKey) || [];
existingParams.push(parameterIndex);
Reflect.defineMetadata("validate", existingParams, target, propertyKey);
}
class GameService {
createGame(
@Validate name: string,
@Validate maxPlayers: number
) {
// Validation happens before method executes
return { name, maxPlayers };
}
}
Decorator factories allow customization:
// Decorator factory with options
function Throttle(milliseconds: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original Method = descriptor.value;
let timeout: NodeJS.Timeout | null = null;
descriptor.value = function (...args: any[]) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
originalMethod.apply(this, args);
}, milliseconds);
};
return descriptor;
};
}
class SearchBox {
@Throttle(300) // Wait 300ms before executing
onSearch(query: string): void {
console.log(`Searching for: ${query}`);
}
}
// Advanced decorator factory
function Cache(duration: number = 60000) {
const cache = new Map<string, { value: any; expires: number }>();
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) {
console.log(`Cache hit for ${propertyKey}`);
return cached.value;
}
const result = originalMethod.apply(this, args);
cache.set(key, {
value: result,
expires: Date.now() + duration
});
return result;
};
return descriptor;
};
}
class DataService {
@Cache(5000) // Cache for 5 seconds
fetchData(id: number): any {
console.log(`Fetching data for ID: ${id}`);
return { id, data: "Some data..." };
}
}
// Dependency Injection (like Angular)
function Injectable() {
return function (constructor: Function) {
// Register service in DI container
console.log(`Registering ${constructor.name} as injectable`);
};
}
@Injectable()
class GameService {
getPlayers() {
return ["Player1", "Player2"];
}
}
// Route decorators (like NestJS)
function Controller(route: string) {
return function (constructor: Function) {
(constructor as any).route = route;
};
}
function Get(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
(descriptor.value as any).route = path;
(descriptor.value as any).method = "GET";
};
}
@Controller('/api/players')
class PlayerController {
@Get('/')
getAll() {
return { players: [] };
}
@Get('/:id')
getOne(id: string) {
return { id, name: "Player" };
}
}
// Entity decorators (like TypeORM)
function Entity(tableName: string) {
return function (constructor: Function) {
(constructor as any).tableName = tableName;
};
}
function Column(options: { type: string; nullable?: boolean }) {
return function (target: any, propertyKey: string) {
// Store column metadata
};
}
@Entity('users')
class UserEntity {
@Column({ type: 'int' })
id: number;
@Column({ type: 'varchar', nullable: false })
username: string;
@Column({ type: 'varchar', nullable: false })
email: string;
}
// Multiple decorators on same target
function First() {
console.log("First(): factory");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("First(): called");
};
}
function Second() {
console.log("Second(): factory");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("Second(): called");
};
}
class Example {
@First()
@Second()
method() {}
}
// Output:
// First(): factory
// Second(): factory
// Second(): called (executed bottom-to-top)
// First(): called
@DecoratorName syntax before declarations