// tsconfig.json — enable decorators
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true, // needed for reflect-metadata
"target": "ES2020"
}
}
// Decorator = function that receives and can modify class/method/property
// Syntax: @decoratorName (above class/method/property)
// Decorator types:
// @ClassDecorator — applied to class
// @MethodDecorator — applied to method
// @PropertyDecorator — applied to property
// @ParameterDecorator— applied to parameter
// Class decorator — receives constructor
function Singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: T | null = null;
return class extends constructor {
constructor(...args: any[]) {
if (instance) return instance as any;
super(...args);
instance = this as any;
}
};
}
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
function Log(constructor: Function) {
console.log(`Class created: ${constructor.name}`);
}
// Decorator factory — returns decorator (can pass args)
function Route(path: string, method: string = 'GET') {
return function(constructor: Function) {
Reflect.defineMetadata('route', path, constructor);
Reflect.defineMetadata('method', method, constructor);
};
}
@Singleton
@Sealed
@Log
class DatabaseService {
constructor(private host: string) {}
connect() { return `Connected to ${this.host}`; }
}
const db1 = new DatabaseService('localhost');
const db2 = new DatabaseService('remote');
console.log(db1 === db2); // true — Singleton!
// Method decorator receives: target, propertyKey, descriptor
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`▶ ${key}(${JSON.stringify(args)})`);
const result = original.apply(this, args);
console.log(`◀ ${key} returned: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
function Memoize(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
const cache = new Map<string, any>();
descriptor.value = function(...args: any[]) {
const cacheKey = JSON.stringify(args);
if (cache.has(cacheKey)) {
console.log(`Cache hit: ${key}(${cacheKey})`);
return cache.get(cacheKey);
}
const result = original.apply(this, args);
cache.set(cacheKey, result);
return result;
};
return descriptor;
}
function Throttle(ms: number) {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
let lastCall = 0;
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
const now = Date.now();
if (now - lastCall >= ms) {
lastCall = now;
return original.apply(this, args);
}
};
return descriptor;
};
}
class MathService {
@Log
add(a: number, b: number): number { return a + b; }
@Memoize
expensiveCalc(n: number): number {
// simulate slow calculation
return n * n * n;
}
@Throttle(1000)
handleClick(): void {
console.log("Button clicked!");
}
}
const svc = new MathService();
svc.add(2, 3); // logs call + result
svc.expensiveCalc(10); // calculates
svc.expensiveCalc(10); // from cache!
function Required(target: any, key: string) {
let value: any;
Object.defineProperty(target, key, {
get() { return value; },
set(newVal) {
if (newVal === null || newVal === undefined || newVal === '') {
throw new Error(`Property '${key}' is required`);
}
value = newVal;
},
enumerable: true,
configurable: true,
});
}
function MinLength(min: number) {
return function(target: any, key: string) {
let value: string;
Object.defineProperty(target, key, {
get() { return value; },
set(v: string) {
if (v.length < min) throw new Error(`${key} must be at least ${min} chars`);
value = v;
},
enumerable: true, configurable: true,
});
};
}
class User {
@Required
name!: string;
@Required
@MinLength(5)
password!: string;
email?: string;
}
const user = new User();
user.name = 'Ko Ko'; // OK
user.password = 'secret123'; // OK (9 chars)
// user.password = '123'; // throws: password must be at least 5 chars
// user.name = ''; // throws: name is required
// Service registry using decorators
const registry = new Map<string, any>();
function Injectable(name: string) {
return function(constructor: new (...args: any[]) => any) {
registry.set(name, constructor);
};
}
function Inject(name: string) {
return function(target: any, key: string) {
Object.defineProperty(target, key, {
get() {
const ServiceClass = registry.get(name);
if (!ServiceClass) throw new Error(`Service '${name}' not registered`);
return new ServiceClass();
},
enumerable: true, configurable: true,
});
};
}
@Injectable('logger')
class Logger {
log(msg: string) { console.log(`[LOG] ${msg}`); }
}
@Injectable('userService')
class UserService {
@Inject('logger') private logger!: Logger;
createUser(name: string) {
this.logger.log(`Creating user: ${name}`);
return { id: Date.now(), name };
}
}
// Usage
const svc = registry.get('userService');
if (svc) {
const service = new svc();
service.createUser('Ko Ko'); // [LOG] Creating user: Ko Ko
}
← TS 06 | Next: TS 08 → Advanced Types →