Prerequisites
- Understanding of decorators basics 📝
- Knowledge of class prototypes 🔍
- Familiarity with constructor functions 💻
What you'll learn
- Create powerful class decorators 🎯
- Modify class constructors and prototypes 🏗️
- Implement mixins via decorators 🛡️
- Build real-world decorator patterns ✨
🎯 Introduction
Welcome to the architectural world of class decorators! 🎉 In this guide, we’ll explore how to use decorators to transform entire classes, adding features, modifying behavior, and creating powerful abstractions at the class level.
You’ll discover how class decorators are like master builders 🏗️ - they can renovate, extend, or completely transform your classes! Whether you’re implementing singleton patterns 🎯, adding logging capabilities 📊, or building ORM mappings 💾, class decorators provide elegant solutions.
By the end of this tutorial, you’ll be confidently creating decorators that enhance your classes with superpowers! Let’s start building! 🏊♂️
📚 Understanding Class Decorators
🤔 How Class Decorators Work
Class decorators are functions that receive the constructor function as their only parameter. They can modify the constructor, its prototype, or even replace the entire class with a new implementation.
Think of class decorators like:
- 🏗️ Architects: Redesigning the blueprint of your building
- 🎨 Painters: Adding new colors and features to existing structures
- 🔧 Mechanics: Installing new engines in your vehicles
- 🧬 Genetic engineers: Modifying DNA to add new traits
💡 Class Decorator Signature
type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
The decorator can either:
- Modify the class in place (return void)
- Return a new class to replace the original
🔧 Basic Class Decorators
📝 Simple Enhancement Patterns
Let’s start with fundamental class decorator patterns:
// 🎯 Sealed class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
console.log(`🔒 Sealed class: ${constructor.name}`);
}
// 🏷️ Adding metadata to classes
function metadata<T extends { new(...args: any[]): {} }>(data: any) {
return function(constructor: T) {
// Add metadata to the constructor
(constructor as any).metadata = data;
// Add instance method to access metadata
constructor.prototype.getMetadata = function() {
return (constructor as any).metadata;
};
console.log(`📊 Added metadata to ${constructor.name}:`, data);
};
}
// 📝 Logger decorator
function withLogging<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
console.log(`🔨 Creating instance of ${constructor.name} with args:`, args);
super(...args);
console.log(`✅ Instance of ${constructor.name} created`);
}
};
}
// 🕐 Timestamp decorator
function timestamped<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
createdAt = new Date();
updatedAt = new Date();
update() {
this.updatedAt = new Date();
if (super.update) {
super.update();
}
}
getAge(): number {
return Date.now() - this.createdAt.getTime();
}
};
}
// 🏠 Using decorators
@sealed
@metadata({ version: '1.0.0', author: 'John Doe' })
@withLogging
@timestamped
class Product {
constructor(
public id: string,
public name: string,
public price: number
) {}
update() {
console.log('Product updated');
}
}
// 💫 Testing
const product = new Product('123', 'Laptop', 999);
console.log('Product age:', product.getAge(), 'ms');
console.log('Metadata:', product.getMetadata());
// Try to extend sealed class (will fail in strict mode)
// class ExtendedProduct extends Product {} // Error!
🏗️ Constructor Modification
Modifying class constructors dynamically:
// 🎯 Auto-ID decorator
let nextId = 1;
function autoId<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
id: number;
constructor(...args: any[]) {
super(...args);
this.id = nextId++;
}
};
}
// 🔐 Singleton decorator
function singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: T;
// Return a new constructor function
const newConstructor: any = function(...args: any[]) {
if (instance) {
console.log('🔄 Returning existing instance');
return instance;
}
console.log('🆕 Creating new instance');
instance = new constructor(...args);
return instance;
};
// Copy prototype properties
newConstructor.prototype = constructor.prototype;
// Copy static properties
Object.setPrototypeOf(newConstructor, constructor);
return newConstructor;
}
// 🏁 Initialization decorator
interface Initializable {
init?(): void | Promise<void>;
}
function autoInit<T extends { new(...args: any[]): Initializable }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
// Auto-call init if it exists
if (this.init) {
const result = this.init();
if (result instanceof Promise) {
result.catch(err => {
console.error('❌ Initialization failed:', err);
});
}
}
}
};
}
// 🏠 Example usage
@autoId
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
@singleton
@autoInit
class Database implements Initializable {
private connected = false;
async init() {
console.log('🔌 Initializing database connection...');
await new Promise(resolve => setTimeout(resolve, 100));
this.connected = true;
console.log('✅ Database connected');
}
isConnected() {
return this.connected;
}
}
// 💫 Testing
const user1 = new User('Alice');
const user2 = new User('Bob');
console.log('User IDs:', user1.id, user2.id); // Different IDs
const db1 = new Database();
const db2 = new Database();
console.log('Same instance?', db1 === db2); // true
🚀 Advanced Class Decorator Patterns
🎭 Mixin Implementation via Decorators
Creating powerful mixin patterns with decorators:
// 🎯 Mixin type helpers
type Constructor<T = {}> = new (...args: any[]) => T;
type Mixin<T extends Constructor> = T;
// 🏃♂️ Movement mixin decorator
interface Movable {
x: number;
y: number;
move(dx: number, dy: number): void;
}
function withMovement<T extends Constructor>(Base: T): Mixin<T & Constructor<Movable>> {
return class extends Base implements Movable {
x = 0;
y = 0;
move(dx: number, dy: number) {
this.x += dx;
this.y += dy;
console.log(`📍 Moved to (${this.x}, ${this.y})`);
}
};
}
// 🎨 Observable mixin decorator
interface Observable {
observers: Map<string, Set<Function>>;
subscribe(event: string, callback: Function): () => void;
notify(event: string, data?: any): void;
}
function withObservable<T extends Constructor>(Base: T): Mixin<T & Constructor<Observable>> {
return class extends Base implements Observable {
observers = new Map<string, Set<Function>>();
subscribe(event: string, callback: Function): () => void {
if (!this.observers.has(event)) {
this.observers.set(event, new Set());
}
this.observers.get(event)!.add(callback);
// Return unsubscribe function
return () => {
this.observers.get(event)?.delete(callback);
};
}
notify(event: string, data?: any) {
this.observers.get(event)?.forEach(callback => {
callback(data);
});
}
};
}
// 💾 Serializable mixin decorator
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
function withSerialization<T extends Constructor>(Base: T): Mixin<T & Constructor<Serializable>> {
return class extends Base implements Serializable {
serialize(): string {
const data: any = {};
// Get all properties
Object.keys(this).forEach(key => {
const value = (this as any)[key];
// Skip functions and complex objects
if (typeof value !== 'function' && !(value instanceof Map) && !(value instanceof Set)) {
data[key] = value;
}
});
return JSON.stringify(data);
}
deserialize(json: string) {
const data = JSON.parse(json);
Object.assign(this, data);
}
};
}
// 🏠 Composed class with multiple mixins
@withSerialization
@withObservable
@withMovement
class GameObject {
constructor(public name: string) {}
destroy() {
this.notify('destroy', { name: this.name });
console.log(`💥 ${this.name} destroyed`);
}
}
// 💫 Usage
const player = new GameObject('Player');
// Observable functionality
const unsubscribe = player.subscribe('destroy', (data) => {
console.log('🔔 Object destroyed:', data);
});
// Movement functionality
player.move(10, 20);
// Serialization functionality
const saved = player.serialize();
console.log('💾 Saved state:', saved);
player.move(5, 5);
const newPlayer = new GameObject('NewPlayer');
newPlayer.deserialize(saved);
console.log('📂 Restored position:', newPlayer.x, newPlayer.y);
player.destroy();
🏗️ Validation and Schema Decorators
Building validation into classes:
// 🎯 Schema definition
interface PropertySchema {
type: 'string' | 'number' | 'boolean' | 'date';
required?: boolean;
min?: number;
max?: number;
pattern?: RegExp;
validator?: (value: any) => boolean;
}
interface ClassSchema {
[property: string]: PropertySchema;
}
// 📊 Schema decorator
function schema(classSchema: ClassSchema) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
// Store schema on constructor
(constructor as any).__schema = classSchema;
// Add validation method
constructor.prototype.validate = function(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
const schema = (constructor as any).__schema;
for (const [prop, propSchema] of Object.entries(schema)) {
const value = (this as any)[prop];
// Required check
if (propSchema.required && (value === undefined || value === null)) {
errors.push(`${prop} is required`);
continue;
}
if (value === undefined || value === null) continue;
// Type check
const actualType = value instanceof Date ? 'date' : typeof value;
if (actualType !== propSchema.type) {
errors.push(`${prop} must be of type ${propSchema.type}`);
continue;
}
// Min/max checks
if (propSchema.min !== undefined && value < propSchema.min) {
errors.push(`${prop} must be at least ${propSchema.min}`);
}
if (propSchema.max !== undefined && value > propSchema.max) {
errors.push(`${prop} must be at most ${propSchema.max}`);
}
// Pattern check
if (propSchema.pattern && !propSchema.pattern.test(value)) {
errors.push(`${prop} does not match required pattern`);
}
// Custom validator
if (propSchema.validator && !propSchema.validator(value)) {
errors.push(`${prop} failed custom validation`);
}
}
return { valid: errors.length === 0, errors };
};
// Add sanitization method
constructor.prototype.sanitize = function() {
const schema = (constructor as any).__schema;
for (const [prop, propSchema] of Object.entries(schema)) {
const value = (this as any)[prop];
if (value === undefined || value === null) continue;
// Type coercion
switch (propSchema.type) {
case 'string':
(this as any)[prop] = String(value).trim();
break;
case 'number':
(this as any)[prop] = Number(value);
break;
case 'boolean':
(this as any)[prop] = Boolean(value);
break;
case 'date':
if (!(value instanceof Date)) {
(this as any)[prop] = new Date(value);
}
break;
}
}
};
return constructor;
};
}
// 🔒 Immutable decorator
function immutable<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
Object.freeze(this);
}
};
}
// 🏠 Example with validation
@schema({
username: {
type: 'string',
required: true,
min: 3,
max: 20,
pattern: /^[a-zA-Z0-9_]+$/
},
email: {
type: 'string',
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
age: {
type: 'number',
min: 0,
max: 150
},
active: {
type: 'boolean',
required: true
}
})
class UserAccount {
username?: string;
email?: string;
age?: number;
active?: boolean;
constructor(data: Partial<UserAccount>) {
Object.assign(this, data);
}
}
// 💫 Testing validation
const account = new UserAccount({
username: 'john_doe',
email: '[email protected]',
age: 25,
active: true
});
const validation = account.validate();
console.log('Valid:', validation.valid);
if (!validation.valid) {
console.log('Errors:', validation.errors);
}
// Test with invalid data
const invalidAccount = new UserAccount({
username: 'ab', // Too short
email: 'invalid-email',
age: 200, // Too old
active: true
});
const invalidValidation = invalidAccount.validate();
console.log('\nInvalid account errors:', invalidValidation.errors);
🎪 Real-World Patterns
🌐 ORM-Style Decorators
Creating database entity decorators:
// 🎯 Entity metadata
interface EntityMetadata {
tableName: string;
columns: Map<string, ColumnMetadata>;
relations: Map<string, RelationMetadata>;
}
interface ColumnMetadata {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
nullable?: boolean;
default?: any;
}
interface RelationMetadata {
type: 'one-to-one' | 'one-to-many' | 'many-to-many';
target: string;
joinColumn?: string;
}
// 📊 Metadata storage
const entityMetadata = new Map<Function, EntityMetadata>();
// 🏗️ Entity decorator
function Entity(tableName: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
// Initialize metadata
const metadata: EntityMetadata = {
tableName,
columns: new Map(),
relations: new Map()
};
entityMetadata.set(constructor, metadata);
// Add repository methods
constructor.prototype.save = async function() {
console.log(`💾 Saving ${tableName} entity:`, this);
// Simulate save operation
return this;
};
constructor.prototype.delete = async function() {
console.log(`🗑️ Deleting ${tableName} entity:`, this);
// Simulate delete operation
};
// Add static repository methods
(constructor as any).find = async function(conditions: any) {
console.log(`🔍 Finding ${tableName} with conditions:`, conditions);
// Simulate find operation
return [];
};
(constructor as any).findOne = async function(conditions: any) {
console.log(`🔍 Finding one ${tableName} with conditions:`, conditions);
// Simulate findOne operation
return new constructor();
};
(constructor as any).create = function(data: any) {
console.log(`🆕 Creating ${tableName} entity`);
return new constructor(data);
};
return constructor;
};
}
// 🔧 Column decorator factory
function Column(options: Partial<ColumnMetadata> = {}) {
return function(target: any, propertyKey: string) {
const constructor = target.constructor;
const metadata = entityMetadata.get(constructor);
if (metadata) {
metadata.columns.set(propertyKey, {
name: options.name || propertyKey,
type: options.type || 'string',
...options
});
}
};
}
// 🔑 Primary key decorator
function PrimaryKey(target: any, propertyKey: string) {
Column({ primaryKey: true })(target, propertyKey);
}
// 🌟 Relation decorators
function OneToMany(targetEntity: string, joinColumn?: string) {
return function(target: any, propertyKey: string) {
const constructor = target.constructor;
const metadata = entityMetadata.get(constructor);
if (metadata) {
metadata.relations.set(propertyKey, {
type: 'one-to-many',
target: targetEntity,
joinColumn
});
}
};
}
function ManyToOne(targetEntity: string, joinColumn?: string) {
return function(target: any, propertyKey: string) {
const constructor = target.constructor;
const metadata = entityMetadata.get(constructor);
if (metadata) {
metadata.relations.set(propertyKey, {
type: 'one-to-many',
target: targetEntity,
joinColumn
});
}
};
}
// 🏠 Example entities
@Entity('users')
class User {
@PrimaryKey
@Column({ type: 'uuid' })
id!: string;
@Column({ unique: true })
username!: string;
@Column({ unique: true })
email!: string;
@Column({ type: 'date', default: () => new Date() })
createdAt!: Date;
@OneToMany('Post', 'userId')
posts!: Post[];
constructor(data?: Partial<User>) {
if (data) Object.assign(this, data);
}
}
@Entity('posts')
class Post {
@PrimaryKey
@Column({ type: 'uuid' })
id!: string;
@Column()
title!: string;
@Column({ type: 'text' })
content!: string;
@Column({ type: 'uuid' })
userId!: string;
@ManyToOne('User', 'userId')
user!: User;
constructor(data?: Partial<Post>) {
if (data) Object.assign(this, data);
}
}
// 💫 Usage
const user = User.create({
id: '123',
username: 'johndoe',
email: '[email protected]'
});
await user.save();
const posts = await Post.find({ userId: '123' });
console.log('Found posts:', posts);
// 🔍 Inspect metadata
function inspectEntity(EntityClass: Function) {
const metadata = entityMetadata.get(EntityClass);
if (!metadata) return;
console.log(`\n📊 Entity: ${EntityClass.name} -> ${metadata.tableName}`);
console.log('Columns:');
metadata.columns.forEach((col, prop) => {
console.log(` - ${prop}: ${col.type}${col.primaryKey ? ' [PK]' : ''}${col.unique ? ' [UNIQUE]' : ''}`);
});
if (metadata.relations.size > 0) {
console.log('Relations:');
metadata.relations.forEach((rel, prop) => {
console.log(` - ${prop}: ${rel.type} -> ${rel.target}`);
});
}
}
inspectEntity(User);
inspectEntity(Post);
🎮 Component System Decorators
Building a game component system:
// 🎯 Component registry
const componentRegistry = new Map<string, Function>();
const componentMetadata = new Map<Function, ComponentMetadata>();
interface ComponentMetadata {
name: string;
updatePriority: number;
dependencies: string[];
}
// 🏗️ Component decorator
function Component(options: Partial<ComponentMetadata> = {}) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
const name = options.name || constructor.name;
const metadata: ComponentMetadata = {
name,
updatePriority: options.updatePriority || 0,
dependencies: options.dependencies || []
};
componentRegistry.set(name, constructor);
componentMetadata.set(constructor, metadata);
// Add component lifecycle methods
if (!constructor.prototype.onAttach) {
constructor.prototype.onAttach = function() {
console.log(`🔗 ${name} attached`);
};
}
if (!constructor.prototype.onDetach) {
constructor.prototype.onDetach = function() {
console.log(`🔓 ${name} detached`);
};
}
if (!constructor.prototype.update) {
constructor.prototype.update = function(deltaTime: number) {
// Default empty update
};
}
return constructor;
};
}
// 🎨 Auto-wire dependencies
function Inject(componentName: string) {
return function(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get() {
const entity = (this as any).entity;
if (!entity) return null;
return entity.getComponent(componentName);
},
enumerable: true,
configurable: true
});
};
}
// 🏠 GameObject with component system
@Component({ name: 'GameObject' })
class GameObject {
private components = new Map<string, any>();
public name: string;
constructor(name: string) {
this.name = name;
}
addComponent(ComponentClass: Function): this {
const metadata = componentMetadata.get(ComponentClass);
if (!metadata) {
throw new Error('Not a valid component class');
}
// Check dependencies
for (const dep of metadata.dependencies) {
if (!this.components.has(dep)) {
throw new Error(`Missing dependency: ${dep}`);
}
}
const component = new (ComponentClass as any)();
component.entity = this;
this.components.set(metadata.name, component);
component.onAttach();
return this;
}
getComponent<T>(name: string): T | null {
return this.components.get(name) || null;
}
removeComponent(name: string): boolean {
const component = this.components.get(name);
if (component) {
component.onDetach();
return this.components.delete(name);
}
return false;
}
update(deltaTime: number): void {
// Sort components by priority
const sorted = Array.from(this.components.entries()).sort((a, b) => {
const aData = componentMetadata.get(a[1].constructor)!;
const bData = componentMetadata.get(b[1].constructor)!;
return bData.updatePriority - aData.updatePriority;
});
// Update in priority order
for (const [name, component] of sorted) {
component.update(deltaTime);
}
}
}
// 🎮 Example components
@Component({ updatePriority: 10 })
class Transform {
x = 0;
y = 0;
rotation = 0;
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
update(deltaTime: number): void {
// Transform updates first (high priority)
}
}
@Component({ dependencies: ['Transform'] })
class Physics {
@Inject('Transform')
transform!: Transform;
velocityX = 0;
velocityY = 0;
gravity = 9.8;
update(deltaTime: number): void {
this.velocityY += this.gravity * deltaTime;
this.transform.move(
this.velocityX * deltaTime,
this.velocityY * deltaTime
);
}
}
@Component({ dependencies: ['Transform'] })
class Renderer {
@Inject('Transform')
transform!: Transform;
color = '#ffffff';
visible = true;
update(deltaTime: number): void {
if (this.visible) {
console.log(`🎨 Rendering at (${this.transform.x.toFixed(2)}, ${this.transform.y.toFixed(2)})`);
}
}
}
// 💫 Usage
const player = new GameObject('Player');
player
.addComponent(Transform)
.addComponent(Physics)
.addComponent(Renderer);
const transform = player.getComponent<Transform>('Transform')!;
transform.move(10, 0);
const physics = player.getComponent<Physics>('Physics')!;
physics.velocityX = 5;
// Simulate game loop
for (let i = 0; i < 3; i++) {
console.log(`\n⏱️ Frame ${i + 1}:`);
player.update(0.016); // 60 FPS
}
🎮 Hands-On Exercise
Let’s build a caching system using class decorators!
📝 Challenge: Smart Caching System
Create a caching system that:
- Automatically caches method results
- Supports TTL (time to live)
- Provides cache statistics
- Allows cache invalidation
// Your challenge: Implement this caching system
interface CacheOptions {
ttl?: number; // Time to live in milliseconds
maxSize?: number; // Maximum cache entries
key?: (...args: any[]) => string; // Custom key generator
}
// Decorators to implement:
// @Cacheable(options) - Class decorator that adds caching
// @CacheKey - Parameter decorator to mark cache key params
// @CacheClear(method) - Method decorator to clear cache
// @CacheStats - Property decorator to expose cache stats
// Example usage to support:
@Cacheable({ ttl: 5000, maxSize: 100 })
class WeatherService {
@CacheStats
stats: any;
async getWeather(@CacheKey city: string, details: boolean = false): Promise<any> {
console.log(`Fetching weather for ${city}...`);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return { city, temp: Math.random() * 30, details };
}
@CacheClear('getWeather')
async updateWeather(city: string, data: any): Promise<void> {
console.log(`Updating weather for ${city}`);
// This should clear cache for the city
}
}
// Implement the decorators!
💡 Solution
Click to see the solution
// 🎯 Cache entry interface
interface CacheEntry {
value: any;
timestamp: number;
hits: number;
}
// 📊 Cache statistics
interface CacheStatistics {
hits: number;
misses: number;
entries: number;
hitRate: number;
evictions: number;
}
// 🔑 Cache key metadata
const CACHE_KEY_METADATA = Symbol('cacheKey');
// 🏗️ Main cacheable decorator
function Cacheable(options: CacheOptions = {}) {
const { ttl = Infinity, maxSize = 100 } = options;
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
// Create cache storage
const cache = new Map<string, CacheEntry>();
const stats: CacheStatistics = {
hits: 0,
misses: 0,
entries: 0,
hitRate: 0,
evictions: 0
};
// Get original methods
const prototype = constructor.prototype;
const propertyNames = Object.getOwnPropertyNames(prototype);
// Cache management functions
const generateKey = (methodName: string, args: any[], keyParams?: number[]): string => {
if (options.key) {
return options.key(...args);
}
// Use only marked parameters for key
const keyArgs = keyParams
? args.filter((_, index) => keyParams.includes(index))
: args;
return `${methodName}:${JSON.stringify(keyArgs)}`;
};
const evictOldest = () => {
let oldestKey: string | null = null;
let oldestTime = Infinity;
cache.forEach((entry, key) => {
if (entry.timestamp < oldestTime) {
oldestTime = entry.timestamp;
oldestKey = key;
}
});
if (oldestKey) {
cache.delete(oldestKey);
stats.evictions++;
stats.entries--;
}
};
const updateHitRate = () => {
const total = stats.hits + stats.misses;
stats.hitRate = total > 0 ? stats.hits / total : 0;
};
// Wrap cacheable methods
propertyNames.forEach(propertyName => {
const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);
if (!descriptor || typeof descriptor.value !== 'function' || propertyName === 'constructor') {
return;
}
const originalMethod = descriptor.value;
// Check for cache key parameters
const cacheKeyParams = Reflect.getMetadata(CACHE_KEY_METADATA, prototype, propertyName);
descriptor.value = async function(...args: any[]) {
const key = generateKey(propertyName, args, cacheKeyParams);
// Check cache
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
cached.hits++;
stats.hits++;
updateHitRate();
console.log(`💾 Cache hit for ${propertyName}`);
return cached.value;
}
// Cache miss
stats.misses++;
updateHitRate();
console.log(`🔄 Cache miss for ${propertyName}`);
// Call original method
const result = await originalMethod.apply(this, args);
// Store in cache
if (cache.size >= maxSize) {
evictOldest();
}
cache.set(key, {
value: result,
timestamp: Date.now(),
hits: 0
});
stats.entries = cache.size;
return result;
};
Object.defineProperty(prototype, propertyName, descriptor);
});
// Add cache management methods
prototype._clearCache = function(methodName?: string) {
if (methodName) {
// Clear specific method cache
const keysToDelete: string[] = [];
cache.forEach((_, key) => {
if (key.startsWith(`${methodName}:`)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach(key => cache.delete(key));
stats.entries = cache.size;
console.log(`🗑️ Cleared cache for ${methodName}`);
} else {
// Clear all cache
cache.clear();
stats.entries = 0;
console.log('🗑️ Cleared all cache');
}
};
prototype._getCacheStats = function(): CacheStatistics {
return { ...stats };
};
prototype._getCacheEntries = function() {
const entries: any[] = [];
cache.forEach((entry, key) => {
entries.push({
key,
age: Date.now() - entry.timestamp,
hits: entry.hits,
value: entry.value
});
});
return entries;
};
return constructor;
};
}
// 🔑 Cache key parameter decorator
function CacheKey(target: any, propertyKey: string | symbol, parameterIndex: number) {
const existingKeys = Reflect.getMetadata(CACHE_KEY_METADATA, target, propertyKey) || [];
existingKeys.push(parameterIndex);
Reflect.defineMetadata(CACHE_KEY_METADATA, existingKeys, target, propertyKey);
}
// 🗑️ Cache clear method decorator
function CacheClear(methodName: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
// Call original method
const result = await originalMethod.apply(this, args);
// Clear cache for specified method
if (this._clearCache) {
this._clearCache(methodName);
}
return result;
};
return descriptor;
};
}
// 📊 Cache statistics property decorator
function CacheStats(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get() {
return this._getCacheStats ? this._getCacheStats() : null;
},
enumerable: true,
configurable: true
});
}
// 🌤️ Example implementation
@Cacheable({ ttl: 5000, maxSize: 100 })
class WeatherService {
@CacheStats
stats!: CacheStatistics;
async getWeather(@CacheKey city: string, details: boolean = false): Promise<any> {
console.log(`🌤️ Fetching weather for ${city}...`);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return {
city,
temp: Math.round(Math.random() * 30),
conditions: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)],
details: details ? { humidity: 65, windSpeed: 10 } : undefined
};
}
async getForecast(@CacheKey city: string, @CacheKey days: number = 7): Promise<any> {
console.log(`📅 Fetching ${days}-day forecast for ${city}...`);
await new Promise(resolve => setTimeout(resolve, 800));
const forecast = [];
for (let i = 0; i < days; i++) {
forecast.push({
day: i,
temp: Math.round(Math.random() * 30),
conditions: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)]
});
}
return { city, days, forecast };
}
@CacheClear('getWeather')
async updateWeather(city: string, data: any): Promise<void> {
console.log(`📝 Updating weather for ${city}:`, data);
// This will automatically clear cache for getWeather method
}
@CacheClear('getForecast')
async updateForecast(city: string, data: any): Promise<void> {
console.log(`📝 Updating forecast for ${city}`);
}
// Expose cache entries for debugging
getCacheInfo(): any {
return (this as any)._getCacheEntries();
}
}
// 💫 Advanced example with custom key generator
@Cacheable({
ttl: 10000,
key: (userId: string, filters: any) => `user:${userId}:${filters.type || 'all'}`
})
class UserDataService {
@CacheStats
stats!: CacheStatistics;
async getUserData(userId: string, filters: { type?: string; limit?: number } = {}): Promise<any> {
console.log(`👤 Fetching data for user ${userId} with filters:`, filters);
await new Promise(resolve => setTimeout(resolve, 500));
return {
userId,
type: filters.type || 'all',
data: Array(filters.limit || 10).fill(null).map((_, i) => ({
id: i,
value: Math.random()
}))
};
}
}
// 🧪 Testing the cache system
async function testCacheSystem() {
console.log('=== Weather Service Cache Test ===\n');
const weather = new WeatherService();
// First calls - cache miss
await weather.getWeather('London');
await weather.getWeather('Paris');
await weather.getForecast('London', 3);
// Second calls - cache hit
await weather.getWeather('London');
await weather.getWeather('Paris');
await weather.getForecast('London', 3);
// Check stats
console.log('\n📊 Cache Statistics:', weather.stats);
console.log('📦 Cache Entries:', weather.getCacheInfo());
// Update and clear cache
await weather.updateWeather('London', { temp: 25 });
// This should miss cache
await weather.getWeather('London');
console.log('\n📊 Final Statistics:', weather.stats);
// Test user data service
console.log('\n\n=== User Data Service Cache Test ===\n');
const userData = new UserDataService();
// Test custom key generation
await userData.getUserData('123', { type: 'posts' });
await userData.getUserData('123', { type: 'comments' });
await userData.getUserData('123', { type: 'posts' }); // Cache hit
console.log('\n📊 User Data Statistics:', userData.stats);
}
testCacheSystem();
🎯 Summary
You’ve mastered class decorators in TypeScript! 🎉 You learned how to:
- 🏗️ Create powerful class decorators that transform entire classes
- 🎨 Modify constructors and prototypes dynamically
- 🧩 Implement mixin patterns using decorators
- 📊 Build validation and schema systems
- 💾 Create ORM-style entity decorators
- ✨ Design real-world patterns like caching and component systems
Class decorators provide elegant ways to enhance your classes with cross-cutting concerns, keeping your code clean and maintainable!
Keep building amazing things with class decorators! 🚀