Prerequisites
- Strong understanding of classes and functions ๐
- Knowledge of higher-order functions ๐
- Familiarity with metadata concepts ๐ป
What you'll learn
- Understand decorator syntax and execution ๐ฏ
- Enable experimental decorators in TypeScript ๐๏ธ
- Work with metadata reflection API ๐ก๏ธ
- Build foundation for advanced decorator patterns โจ
๐ฏ Introduction
Welcome to the magical world of decorators! ๐ In this guide, weโll explore TypeScriptโs decorator feature - a powerful way to modify classes, methods, and properties using a declarative syntax inspired by annotations in other languages.
Youโll discover how decorators are like enchantments ๐ช - they add special powers to your code without changing its core structure! Whether youโre building frameworks ๐๏ธ, implementing aspect-oriented programming ๐ฏ, or adding metadata for runtime reflection ๐, understanding decorators opens up metaprogramming possibilities.
By the end of this tutorial, youโll understand the decorator execution model, metadata reflection, and be ready to create your own decorators! Letโs add some magic to our code! ๐โโ๏ธ
๐ Understanding Decorators
๐ค What are Decorators?
Decorators are special declarations that can be attached to classes, methods, properties, or parameters. They use the @expression
syntax and are essentially functions that execute at runtime to modify or annotate the target theyโre applied to.
Think of decorators like:
- ๐ช Magic spells: Enhancing objects with special abilities
- ๐ท๏ธ Labels: Adding metadata tags to code elements
- ๐ Gift wrapping: Adding layers of functionality
- ๐ง Power tools: Modifying behavior without touching internals
๐ก Why Use Decorators?
Hereโs why developers love decorators:
- Declarative Syntax ๐: Clear, readable way to add functionality
- Separation of Concerns ๐ฏ: Keep cross-cutting concerns separate
- Reusability โป๏ธ: Apply the same enhancement multiple times
- Framework Building ๐๏ธ: Essential for modern frameworks
Real-world examples: Angularโs @Component
๐
ฐ๏ธ, NestJSโs @Controller
๐ฎ, TypeORMโs @Entity
๐พ - decorators are everywhere in modern TypeScript frameworks!
๐ง Setting Up Decorators
๐ Enabling Decorators
First, letโs configure TypeScript for decorators:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"moduleResolution": "node"
}
}
๐จ Basic Decorator Syntax
Letโs explore the fundamental patterns:
// ๐ฏ Simple logging decorator
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;
}
// ๐ Using the decorator
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);
// Output:
// Calling add with args: [5, 3]
// add returned: 8
// ๐ฏ Decorator factories
function logWithPrefix(prefix: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[${prefix}] Calling ${propertyKey}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class Service {
@logWithPrefix('API')
fetchData(): string {
return 'data';
}
@logWithPrefix('DB')
saveData(data: string): void {
console.log('Saving:', data);
}
}
๐ Decorator Types and Execution Order
๐ Different Decorator Types
TypeScript supports five types of decorators:
// ๐๏ธ Class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
console.log(`Sealed class: ${constructor.name}`);
}
// ๐ง Method decorator
function enumerable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
console.log(`Made ${propertyKey} enumerable: ${value}`);
};
}
// ๐ฆ Property decorator
function readonly(target: any, propertyKey: string) {
console.log(`Making ${propertyKey} readonly`);
let value = target[propertyKey];
const getter = () => value;
const setter = (newVal: any) => {
console.log(`Cannot set readonly property ${propertyKey}`);
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
// ๐ฏ Parameter decorator
function required(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter ${parameterIndex} of ${propertyKey} is required`);
// Store metadata about required parameters
const existingRequiredParameters = Reflect.getOwnMetadata('required', target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata('required', existingRequiredParameters, target, propertyKey);
}
// ๐ Accessor decorator
function configurable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
console.log(`Made ${propertyKey} configurable: ${value}`);
};
}
// ๐ซ Using all decorator types
@sealed
class User {
@readonly
id: string = 'user_001';
private _name: string = '';
@configurable(false)
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
@enumerable(false)
updateProfile(
@required name: string,
@required email: string,
bio?: string
): void {
console.log('Updating profile...');
}
}
// ๐ฏ Execution order demonstration
function first() {
console.log('first(): factory evaluated');
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('first(): called');
};
}
function second() {
console.log('second(): factory evaluated');
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('second(): called');
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// Output:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
๐ Metadata Reflection API
๐ Working with Metadata
Using the Reflect Metadata API for powerful runtime introspection:
// First, install: npm install reflect-metadata
import 'reflect-metadata';
// ๐ฏ Custom metadata decorators
const MetadataKeys = {
ROUTE: 'route',
METHOD: 'method',
MIDDLEWARE: 'middleware',
ROLES: 'roles',
VALIDATION: 'validation'
} as const;
// ๐ Route decorator
function Route(path: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(MetadataKeys.ROUTE, path, target, propertyKey);
};
}
// ๐ง HTTP method decorators
function Get(path: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(MetadataKeys.ROUTE, path, target, propertyKey);
Reflect.defineMetadata(MetadataKeys.METHOD, 'GET', target, propertyKey);
};
}
function Post(path: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(MetadataKeys.ROUTE, path, target, propertyKey);
Reflect.defineMetadata(MetadataKeys.METHOD, 'POST', target, propertyKey);
};
}
// ๐ก๏ธ Middleware decorator
function UseMiddleware(...middleware: Function[]) {
return function(target: any, propertyKey?: string) {
if (propertyKey) {
// Method decorator
Reflect.defineMetadata(MetadataKeys.MIDDLEWARE, middleware, target, propertyKey);
} else {
// Class decorator
Reflect.defineMetadata(MetadataKeys.MIDDLEWARE, middleware, target);
}
};
}
// ๐ Role-based access decorator
function RequireRoles(...roles: string[]) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(MetadataKeys.ROLES, roles, target, propertyKey);
};
}
// ๐ Validation decorator
interface ValidationRule {
validator: (value: any) => boolean;
message: string;
}
function Validate(rules: ValidationRule[]) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(MetadataKeys.VALIDATION, rules, target, propertyKey);
};
}
// ๐๏ธ Controller using metadata
@UseMiddleware(authMiddleware, loggingMiddleware)
class UserController {
@Get('/users')
@RequireRoles('admin', 'user')
getAllUsers() {
return { users: [] };
}
@Post('/users')
@RequireRoles('admin')
@UseMiddleware(validationMiddleware)
@Validate([
{ validator: (data) => data.email?.includes('@'), message: 'Invalid email' },
{ validator: (data) => data.password?.length >= 8, message: 'Password too short' }
])
createUser(data: any) {
return { id: '123', ...data };
}
@Get('/users/:id')
@RequireRoles('admin', 'user')
getUser(id: string) {
return { id, name: 'John Doe' };
}
}
// ๐ Reading metadata
function analyzeController(controller: any) {
const className = controller.constructor.name;
console.log(`\nAnalyzing ${className}:`);
// Class-level middleware
const classMiddleware = Reflect.getMetadata(MetadataKeys.MIDDLEWARE, controller.constructor) || [];
console.log(`Class middleware: ${classMiddleware.map(m => m.name).join(', ')}`);
// Analyze methods
const prototype = controller.constructor.prototype;
const methodNames = Object.getOwnPropertyNames(prototype)
.filter(name => name !== 'constructor');
methodNames.forEach(methodName => {
const route = Reflect.getMetadata(MetadataKeys.ROUTE, prototype, methodName);
const method = Reflect.getMetadata(MetadataKeys.METHOD, prototype, methodName);
const roles = Reflect.getMetadata(MetadataKeys.ROLES, prototype, methodName);
const middleware = Reflect.getMetadata(MetadataKeys.MIDDLEWARE, prototype, methodName);
const validation = Reflect.getMetadata(MetadataKeys.VALIDATION, prototype, methodName);
if (route) {
console.log(`\n ${methodName}():`);
console.log(` Route: ${method || 'GET'} ${route}`);
if (roles) console.log(` Roles: ${roles.join(', ')}`);
if (middleware) console.log(` Middleware: ${middleware.map(m => m.name).join(', ')}`);
if (validation) console.log(` Validation rules: ${validation.length}`);
}
});
}
// Mock middleware functions
function authMiddleware() {}
function loggingMiddleware() {}
function validationMiddleware() {}
const controller = new UserController();
analyzeController(controller);
๐ญ Advanced Decorator Patterns
๐ Decorator Composition
Building complex behaviors through composition:
// ๐ฏ Composable decorators
function compose(...decorators: MethodDecorator[]): MethodDecorator {
return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
decorators.forEach(decorator => decorator(target, propertyKey, descriptor));
return descriptor;
};
}
// โฑ๏ธ Performance monitoring decorator
function measureTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
const end = performance.now();
console.log(`โฑ๏ธ ${propertyKey} took ${(end - start).toFixed(2)}ms`);
return result;
} catch (error) {
const end = performance.now();
console.log(`โฑ๏ธ ${propertyKey} failed after ${(end - start).toFixed(2)}ms`);
throw error;
}
};
}
// ๐พ Caching decorator
function cache(ttl: number = 60000) {
const cacheMap = new Map<string, { value: any; timestamp: 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 = cacheMap.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log(`๐พ Cache hit for ${propertyKey}`);
return cached.value;
}
const result = originalMethod.apply(this, args);
cacheMap.set(key, { value: result, timestamp: Date.now() });
console.log(`๐พ Cached result for ${propertyKey}`);
return result;
};
};
}
// ๐ Retry decorator
function retry(attempts: number = 3, delay: number = 1000) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
for (let i = 0; i < attempts; i++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
console.log(`๐ Attempt ${i + 1} failed for ${propertyKey}`);
if (i === attempts - 1) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
};
}
// ๐ก๏ธ Error handling decorator
function handleErrors(handler?: (error: Error) => any) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
console.error(`โ Error in ${propertyKey}:`, error);
if (handler) {
return handler(error as Error);
}
throw error;
}
};
};
}
// ๐๏ธ Service using composed decorators
class DataService {
@compose(
measureTime,
handleErrors(() => ({ error: 'Service unavailable' })),
retry(3, 500),
cache(5000)
)
async fetchData(id: string): Promise<any> {
// Simulate API call
if (Math.random() > 0.7) {
throw new Error('Network error');
}
await new Promise(resolve => setTimeout(resolve, 100));
return { id, data: `Data for ${id}` };
}
@measureTime
@cache(10000)
processData(data: any): any {
// Simulate expensive computation
const start = Date.now();
while (Date.now() - start < 50) {} // Busy wait
return { ...data, processed: true };
}
}
// ๐ซ Usage
const service = new DataService();
async function testService() {
// First call - will fetch
await service.fetchData('123');
// Second call - cache hit
await service.fetchData('123');
// Process data
service.processData({ value: 42 });
service.processData({ value: 42 }); // Cache hit
}
๐๏ธ Property Injection Pattern
Implementing dependency injection with decorators:
// ๐ฏ Dependency injection system
const container = new Map<string, any>();
function Injectable(token?: string) {
return function(target: any) {
const t = token || target.name;
container.set(t, target);
console.log(`๐ Registered ${t} in container`);
};
}
function Inject(token: string) {
return function(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get() {
const service = container.get(token);
if (!service) {
throw new Error(`Service ${token} not found in container`);
}
return new service();
},
enumerable: true,
configurable: true
});
};
}
// ๐ง Auto-wire dependencies
function AutoWired(target: any, propertyKey: string) {
const type = Reflect.getMetadata('design:type', target, propertyKey);
Object.defineProperty(target, propertyKey, {
get() {
const service = container.get(type.name);
if (!service) {
throw new Error(`Service ${type.name} not found for auto-wiring`);
}
return new service();
},
enumerable: true,
configurable: true
});
}
// ๐ Services with dependency injection
@Injectable()
class LoggerService {
log(message: string): void {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}
}
@Injectable()
class DatabaseService {
@AutoWired
private logger!: LoggerService;
query(sql: string): any {
this.logger.log(`Executing query: ${sql}`);
return { rows: [] };
}
}
@Injectable('EmailService')
class EmailService {
@AutoWired
private logger!: LoggerService;
send(to: string, subject: string, body: string): void {
this.logger.log(`Sending email to ${to}: ${subject}`);
}
}
// ๐ Main application class
class Application {
@AutoWired
private database!: DatabaseService;
@Inject('EmailService')
private email!: EmailService;
@AutoWired
private logger!: LoggerService;
run(): void {
this.logger.log('Application starting...');
const users = this.database.query('SELECT * FROM users');
this.email.send('[email protected]', 'App Started', 'The application has started');
this.logger.log('Application ready!');
}
}
// ๐ซ Usage
const app = new Application();
app.run();
๐จ Building a Mini Framework
๐๏ธ Creating a Web Framework with Decorators
Letโs build a simple Express-like framework:
// ๐ Mini web framework
interface Route {
method: string;
path: string;
handler: Function;
middleware: Function[];
}
class Router {
private routes: Route[] = [];
addRoute(method: string, path: string, handler: Function, middleware: Function[] = []): void {
this.routes.push({ method, path, handler, middleware });
}
getRoutes(): Route[] {
return this.routes;
}
}
const globalRouter = new Router();
// ๐ฏ Controller decorator
function Controller(basePath: string = '') {
return function(target: any) {
const router = new Router();
const prototype = target.prototype;
// Collect all routes from methods
Object.getOwnPropertyNames(prototype).forEach(methodName => {
if (methodName === 'constructor') return;
const routePath = Reflect.getMetadata('path', prototype, methodName);
const httpMethod = Reflect.getMetadata('method', prototype, methodName);
const middleware = Reflect.getMetadata('middleware', prototype, methodName) || [];
if (routePath && httpMethod) {
const fullPath = basePath + routePath;
const handler = prototype[methodName].bind(prototype);
router.addRoute(httpMethod, fullPath, handler, middleware);
globalRouter.addRoute(httpMethod, fullPath, handler, middleware);
}
});
// Store router on the class
Reflect.defineMetadata('router', router, target);
};
}
// ๐ง HTTP method decorators
function createMethodDecorator(method: string) {
return function(path: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
Reflect.defineMetadata('path', path, target, propertyKey);
Reflect.defineMetadata('method', method, target, propertyKey);
};
};
}
const Get = createMethodDecorator('GET');
const Post = createMethodDecorator('POST');
const Put = createMethodDecorator('PUT');
const Delete = createMethodDecorator('DELETE');
const Patch = createMethodDecorator('PATCH');
// ๐ก๏ธ Middleware decorator
function Use(...middleware: Function[]) {
return function(target: any, propertyKey?: string) {
if (propertyKey) {
const existing = Reflect.getMetadata('middleware', target, propertyKey) || [];
Reflect.defineMetadata('middleware', [...existing, ...middleware], target, propertyKey);
} else {
// Class-level middleware
const existing = Reflect.getMetadata('middleware', target) || [];
Reflect.defineMetadata('middleware', [...existing, ...middleware], target);
}
};
}
// ๐ Auth decorators
function RequireAuth(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const middleware = Reflect.getMetadata('middleware', target, propertyKey) || [];
middleware.push(authMiddleware);
Reflect.defineMetadata('middleware', middleware, target, propertyKey);
}
function RequireRole(role: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const middleware = Reflect.getMetadata('middleware', target, propertyKey) || [];
middleware.push((req: any) => checkRole(req, role));
Reflect.defineMetadata('middleware', middleware, target, propertyKey);
};
}
// ๐ Request/Response types
interface Request {
params: Record<string, string>;
query: Record<string, string>;
body: any;
headers: Record<string, string>;
user?: any;
}
interface Response {
status(code: number): Response;
json(data: any): void;
send(data: string): void;
}
// ๐ Example controllers
@Controller('/api/users')
@Use(loggingMiddleware)
class UserController {
@Get('/')
getAllUsers(req: Request, res: Response): void {
res.json({ users: ['Alice', 'Bob', 'Charlie'] });
}
@Get('/:id')
@RequireAuth
getUser(req: Request, res: Response): void {
const { id } = req.params;
res.json({ id, name: 'John Doe' });
}
@Post('/')
@RequireRole('admin')
@Use(validationMiddleware)
createUser(req: Request, res: Response): void {
const user = req.body;
res.status(201).json({ id: '123', ...user });
}
@Put('/:id')
@RequireAuth
updateUser(req: Request, res: Response): void {
const { id } = req.params;
res.json({ id, ...req.body, updated: true });
}
@Delete('/:id')
@RequireRole('admin')
deleteUser(req: Request, res: Response): void {
const { id } = req.params;
res.json({ deleted: true, id });
}
}
@Controller('/api/posts')
class PostController {
@Get('/')
getPosts(req: Request, res: Response): void {
res.json({ posts: [] });
}
@Post('/')
@RequireAuth
createPost(req: Request, res: Response): void {
res.status(201).json({ id: '456', ...req.body });
}
}
// ๐ Framework initialization
function createApp() {
const routes = globalRouter.getRoutes();
console.log('\n๐ Registered Routes:');
routes.forEach(route => {
const middlewareNames = route.middleware.map(m => m.name).join(', ');
console.log(` ${route.method} ${route.path} [${middlewareNames}]`);
});
return {
routes,
listen(port: number) {
console.log(`\n๐ Server listening on port ${port}`);
}
};
}
// Mock middleware
function loggingMiddleware(req: Request) {
console.log(`๐ ${req.method} ${req.path}`);
}
function authMiddleware(req: Request) {
if (!req.headers.authorization) {
throw new Error('Unauthorized');
}
}
function validationMiddleware(req: Request) {
// Validate request body
}
function checkRole(req: Request, role: string) {
if (req.user?.role !== role) {
throw new Error(`Requires role: ${role}`);
}
}
// ๐ซ Initialize app
const app = createApp();
app.listen(3000);
๐ฎ Hands-On Exercise
Letโs build a validation system using decorators!
๐ Challenge: Type-Safe Validation System
Create a validation system that:
- Uses decorators to define validation rules
- Supports custom validators
- Collects and runs validations
- Provides detailed error messages
// Your challenge: Implement this validation system
interface ValidationResult {
valid: boolean;
errors: Array<{ property: string; message: string }>;
}
// Decorators to implement:
// @IsRequired - Property must have a value
// @MinLength(n) - String must be at least n characters
// @MaxLength(n) - String must be at most n characters
// @IsEmail - Must be valid email format
// @Min(n) - Number must be >= n
// @Max(n) - Number must be <= n
// @Pattern(regex) - Must match regex pattern
// @Custom(validator) - Custom validation function
// Example usage to support:
class UserRegistration {
@IsRequired
@MinLength(3)
@MaxLength(20)
username!: string;
@IsRequired
@IsEmail
email!: string;
@IsRequired
@MinLength(8)
@Pattern(/^(?=.*[A-Za-z])(?=.*\d)/)
password!: string;
@Min(18)
@Max(120)
age?: number;
@Custom((value) => value !== 'admin')
role?: string;
}
// Implement the validation function
function validate(instance: any): ValidationResult {
// Your implementation
}
๐ก Solution
Click to see the solution
import 'reflect-metadata';
// ๐ฏ Validation metadata key
const VALIDATION_KEY = Symbol('validations');
// ๐ Validation rule interface
interface ValidationRule {
validator: (value: any, instance?: any) => boolean;
message: (propertyName: string, value: any) => string;
}
// ๐๏ธ Base validation decorator factory
function addValidation(rule: ValidationRule) {
return function(target: any, propertyKey: string) {
const existingRules = Reflect.getMetadata(VALIDATION_KEY, target, propertyKey) || [];
existingRules.push(rule);
Reflect.defineMetadata(VALIDATION_KEY, existingRules, target, propertyKey);
};
}
// โ
Required validator
function IsRequired(target: any, propertyKey: string) {
addValidation({
validator: (value) => value !== undefined && value !== null && value !== '',
message: (prop) => `${prop} is required`
})(target, propertyKey);
}
// ๐ Length validators
function MinLength(min: number) {
return addValidation({
validator: (value) => !value || value.length >= min,
message: (prop, value) => `${prop} must be at least ${min} characters (got ${value?.length || 0})`
});
}
function MaxLength(max: number) {
return addValidation({
validator: (value) => !value || value.length <= max,
message: (prop, value) => `${prop} must be at most ${max} characters (got ${value?.length || 0})`
});
}
// ๐ง Email validator
function IsEmail(target: any, propertyKey: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
addValidation({
validator: (value) => !value || emailRegex.test(value),
message: (prop, value) => `${prop} must be a valid email address (got "${value}")`
})(target, propertyKey);
}
// ๐ข Number validators
function Min(min: number) {
return addValidation({
validator: (value) => value === undefined || value >= min,
message: (prop, value) => `${prop} must be at least ${min} (got ${value})`
});
}
function Max(max: number) {
return addValidation({
validator: (value) => value === undefined || value <= max,
message: (prop, value) => `${prop} must be at most ${max} (got ${value})`
});
}
// ๐ฏ Pattern validator
function Pattern(pattern: RegExp, message?: string) {
return addValidation({
validator: (value) => !value || pattern.test(value),
message: (prop, value) => message || `${prop} must match pattern ${pattern} (got "${value}")`
});
}
// ๐ง Custom validator
function Custom(
validator: (value: any, instance?: any) => boolean,
message?: string | ((prop: string, value: any) => string)
) {
return addValidation({
validator,
message: typeof message === 'function'
? message
: (prop, value) => message || `${prop} failed custom validation`
});
}
// ๐จ Composite validators
function IsAlphanumeric(target: any, propertyKey: string) {
Pattern(/^[a-zA-Z0-9]+$/, `${propertyKey} must contain only letters and numbers`)(target, propertyKey);
}
function IsUrl(target: any, propertyKey: string) {
const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
Pattern(urlRegex, `${propertyKey} must be a valid URL`)(target, propertyKey);
}
// ๐๏ธ Validation result interface
interface ValidationResult {
valid: boolean;
errors: Array<{ property: string; message: string }>;
}
// ๐ Validation function
function validate(instance: any): ValidationResult {
const errors: Array<{ property: string; message: string }> = [];
const prototype = Object.getPrototypeOf(instance);
// Get all properties
const properties = [
...Object.getOwnPropertyNames(instance),
...Object.getOwnPropertyNames(prototype)
].filter(prop => prop !== 'constructor');
// Validate each property
for (const propertyKey of properties) {
const rules = Reflect.getMetadata(VALIDATION_KEY, prototype, propertyKey) || [];
const value = instance[propertyKey];
for (const rule of rules) {
if (!rule.validator(value, instance)) {
errors.push({
property: propertyKey,
message: rule.message(propertyKey, value)
});
}
}
}
return {
valid: errors.length === 0,
errors
};
}
// ๐ฏ Advanced validation decorators
function ValidateNested(target: any, propertyKey: string) {
addValidation({
validator: (value) => {
if (!value) return true;
const result = validate(value);
return result.valid;
},
message: (prop, value) => {
if (!value) return `${prop} is invalid`;
const result = validate(value);
return `${prop} has validation errors: ${result.errors.map(e => e.message).join(', ')}`;
}
})(target, propertyKey);
}
function ArrayMinSize(min: number) {
return addValidation({
validator: (value) => !value || (Array.isArray(value) && value.length >= min),
message: (prop, value) => `${prop} must have at least ${min} items (got ${value?.length || 0})`
});
}
function ArrayMaxSize(max: number) {
return addValidation({
validator: (value) => !value || (Array.isArray(value) && value.length <= max),
message: (prop, value) => `${prop} must have at most ${max} items (got ${value?.length || 0})`
});
}
// ๐ซ Example usage
class Address {
@IsRequired
street!: string;
@IsRequired
city!: string;
@Pattern(/^\d{5}$/, 'Zip code must be 5 digits')
zipCode!: string;
}
class UserRegistration {
@IsRequired
@MinLength(3)
@MaxLength(20)
@IsAlphanumeric
username!: string;
@IsRequired
@IsEmail
email!: string;
@IsRequired
@MinLength(8)
@Pattern(
/^(?=.*[A-Za-z])(?=.*\d)/,
'Password must contain at least one letter and one number'
)
password!: string;
@Min(18)
@Max(120)
age?: number;
@Custom(
(value) => !value || value !== 'admin',
'Role cannot be "admin"'
)
role?: string;
@IsUrl
website?: string;
@ValidateNested
address?: Address;
@ArrayMinSize(1)
@ArrayMaxSize(5)
@Custom(
(value) => !value || value.every((tag: string) => tag.length > 0),
'All tags must be non-empty'
)
tags?: string[];
}
// ๐งช Test the validation system
console.log('=== Validation System Demo ===\n');
// Test 1: Invalid registration
const user1 = new UserRegistration();
user1.username = 'ab'; // Too short
user1.email = 'invalid-email';
user1.password = 'weak';
user1.age = 200;
user1.role = 'admin';
user1.website = 'not-a-url';
user1.tags = ['', 'tag2'];
console.log('Test 1 - Invalid data:');
const result1 = validate(user1);
console.log(`Valid: ${result1.valid}`);
result1.errors.forEach(error => {
console.log(` โ ${error.message}`);
});
// Test 2: Valid registration
const user2 = new UserRegistration();
user2.username = 'john123';
user2.email = '[email protected]';
user2.password = 'SecurePass123';
user2.age = 25;
user2.role = 'user';
user2.website = 'https://example.com';
user2.address = new Address();
user2.address.street = '123 Main St';
user2.address.city = 'New York';
user2.address.zipCode = '12345';
user2.tags = ['coding', 'typescript'];
console.log('\nTest 2 - Valid data:');
const result2 = validate(user2);
console.log(`Valid: ${result2.valid}`);
if (result2.errors.length > 0) {
result2.errors.forEach(error => {
console.log(` โ ${error.message}`);
});
} else {
console.log(' โ
All validations passed!');
}
// ๐ง Validation helper function
function assertValid<T>(instance: T): asserts instance is T {
const result = validate(instance);
if (!result.valid) {
const errorMessages = result.errors.map(e => ` - ${e.message}`).join('\n');
throw new Error(`Validation failed:\n${errorMessages}`);
}
}
// Usage with type narrowing
try {
assertValid(user2);
console.log('\nโ
User registration is valid and type-safe!');
} catch (error) {
console.error('โ Validation error:', error.message);
}
๐ฏ Summary
Youโve mastered the fundamentals of TypeScript decorators! ๐ You learned how to:
- ๐จ Enable and use decorators in TypeScript
- ๐ง Create different types of decorators (class, method, property, parameter)
- ๐ Work with metadata reflection API
- ๐๏ธ Build decorator factories and compose decorators
- ๐ Implement dependency injection patterns
- โจ Create mini-frameworks using decorators
Decorators provide a powerful way to add metadata and modify behavior declaratively, making them essential for building modern TypeScript applications and frameworks!
Keep exploring the power of metaprogramming with decorators! ๐