Write and use TypeScript decorators: class, method, property, and parameter decorators. Build a dependency injection container and validation framework using decorators.
Time
35 minutes
Prerequisites
Lab 04 (Classes), Lab 06 (Generics)
Tools
Docker image: zchencow/innozverse-ts:latest
Note: Requires "experimentalDecorators": true in tsconfig (enabled in the Docker image)
Lab Instructions
Step 1: Class Decorators
💡 Decorators execute bottom-up when stacked — @log("FACTORY") runs after @sealed. Class decorators receive the constructor function and can return a new class that extends it. This is how Angular's @Component, @Injectable, and @NgModule work.
📸 Verified Output:
Step 2: Method & Property Decorators
💡 Method decorators receive the class prototype, method name, and PropertyDescriptor. Replacing descriptor.value replaces the method with a wrapper. This is how @UseGuards, @Roles, @Cache work in NestJS — they wrap methods without modifying the class source code.
📸 Verified Output:
Steps 3–8: Property, Parameter, Reflect, DI Container, Validation Framework, Capstone
📸 Verified Output:
Summary
TypeScript decorators enable declarative programming — you annotate classes and methods with @decorator instead of modifying them directly. You've built memoization, validation, timing, a DI container, and a DTO validation system. This is exactly how Angular, NestJS, TypeORM, and class-validator work.
// Decorator is a function that receives the decorated target
function sealed(constructor: Function): void {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
function singleton<T extends new (...args: unknown[]) => object>(Ctor: T): T {
let instance: InstanceType<T>;
return class extends Ctor {
constructor(...args: unknown[]) {
if (instance) return instance;
super(...args);
instance = this as InstanceType<T>;
}
} as T;
}
function log(prefix: string) {
return function<T extends new (...args: unknown[]) => object>(Ctor: T): T {
return class extends Ctor {
constructor(...args: unknown[]) {
console.log(`[${prefix}] Creating ${Ctor.name}`);
super(...args);
}
} as T;
};
}
@log("FACTORY")
@sealed
class DatabaseConnection {
constructor(private host: string, private port: number) {}
toString() { return `DB(${this.host}:${this.port})`; }
}
@singleton
class AppConfig {
private data: Record<string, string> = {};
set(key: string, val: string) { this.data[key] = val; }
get(key: string) { return this.data[key]; }
}
const db = new DatabaseConnection("localhost", 5432);
console.log(db.toString());
const cfg1 = new AppConfig();
const cfg2 = new AppConfig();
cfg1.set("theme", "dark");
console.log("Same instance:", cfg2.get("theme")); // dark
docker run --rm zchencow/innozverse-ts:latest ts-node --experimentalDecorators -e "
function timestamp<T extends new (...args: unknown[]) => object>(Ctor: T): T {
return class extends Ctor {
createdAt = new Date().toISOString();
} as T;
}
@timestamp
class User { constructor(public name: string) {} }
const u = new User('Dr. Chen');
console.log(u.name, (u as any).createdAt.slice(0, 10));
"
Dr. Chen 2026-03-03
// Method decorator — intercept method calls
function memoize(target: object, key: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const original = descriptor.value;
const cache = new Map<string, unknown>();
descriptor.value = function(...args: unknown[]) {
const cacheKey = JSON.stringify(args);
if (!cache.has(cacheKey)) {
console.log(` [memo] Computing ${key}(${args.join(",")})`);
cache.set(cacheKey, original.apply(this, args));
}
return cache.get(cacheKey);
};
return descriptor;
}
function validate(validator: (val: unknown) => boolean, message: string) {
return function(target: object, key: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const original = descriptor.value;
descriptor.value = function(...args: unknown[]) {
if (!validator(args[0])) throw new Error(`${message} (got: ${args[0]})`);
return original.apply(this, args);
};
return descriptor;
};
}
function readonly(target: object, key: string, descriptor: PropertyDescriptor): PropertyDescriptor {
descriptor.writable = false;
return descriptor;
}
class MathService {
@memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
@validate(n => typeof n === "number" && n > 0, "Amount must be positive")
calculateTax(amount: number): number {
return amount * 0.08;
}
}
const math = new MathService();
console.log("fib(10):", math.fibonacci(10));
console.log("fib(10):", math.fibonacci(10)); // from cache
console.log("tax(864):", math.calculateTax(864));
try { math.calculateTax(-100); }
catch (e) { console.log("Error:", (e as Error).message); }
[memo] Computing fibonacci(10)
fib(10): 55
fib(10): 55
tax(864): 69.12
Error: Amount must be positive (got: -100)