Back to Blog
TypeScriptDecoratorsPatternsAdvanced

TypeScript Decorators: A Practical Guide

Master TypeScript decorators. From class decorators to method decorators to real-world patterns.

B
Bootspring Team
Engineering
July 20, 2022
6 min read

Decorators modify classes and their members at design time. Here's how to use them effectively.

Enabling Decorators#

1// tsconfig.json 2{ 3 "compilerOptions": { 4 "experimentalDecorators": true, 5 "emitDecoratorMetadata": true 6 } 7}

Class Decorators#

1// Simple class decorator 2function Sealed(constructor: Function) { 3 Object.seal(constructor); 4 Object.seal(constructor.prototype); 5} 6 7@Sealed 8class User { 9 name: string; 10 11 constructor(name: string) { 12 this.name = name; 13 } 14} 15 16// Class decorator factory 17function Entity(tableName: string) { 18 return function <T extends { new (...args: any[]): {} }>(constructor: T) { 19 return class extends constructor { 20 static tableName = tableName; 21 }; 22 }; 23} 24 25@Entity('users') 26class User { 27 id: string; 28 name: string; 29} 30 31console.log((User as any).tableName); // 'users' 32 33// Decorator that adds methods 34function Timestamped<T extends { new (...args: any[]): {} }>(Base: T) { 35 return class extends Base { 36 createdAt = new Date(); 37 updatedAt = new Date(); 38 39 touch() { 40 this.updatedAt = new Date(); 41 } 42 }; 43} 44 45@Timestamped 46class Post { 47 title: string; 48 49 constructor(title: string) { 50 this.title = title; 51 } 52} 53 54const post = new Post('Hello'); 55console.log(post.createdAt); // Date

Method Decorators#

1// Log method calls 2function Log( 3 target: any, 4 propertyKey: string, 5 descriptor: PropertyDescriptor 6) { 7 const original = descriptor.value; 8 9 descriptor.value = function (...args: any[]) { 10 console.log(`Calling ${propertyKey} with:`, args); 11 const result = original.apply(this, args); 12 console.log(`Result:`, result); 13 return result; 14 }; 15 16 return descriptor; 17} 18 19class Calculator { 20 @Log 21 add(a: number, b: number): number { 22 return a + b; 23 } 24} 25 26// Measure execution time 27function Measure( 28 target: any, 29 propertyKey: string, 30 descriptor: PropertyDescriptor 31) { 32 const original = descriptor.value; 33 34 descriptor.value = async function (...args: any[]) { 35 const start = performance.now(); 36 const result = await original.apply(this, args); 37 const duration = performance.now() - start; 38 console.log(`${propertyKey} took ${duration.toFixed(2)}ms`); 39 return result; 40 }; 41 42 return descriptor; 43} 44 45class DataService { 46 @Measure 47 async fetchData(): Promise<Data[]> { 48 // Fetch data... 49 } 50} 51 52// Memoization decorator 53function Memoize( 54 target: any, 55 propertyKey: string, 56 descriptor: PropertyDescriptor 57) { 58 const cache = new Map<string, any>(); 59 const original = descriptor.value; 60 61 descriptor.value = function (...args: any[]) { 62 const key = JSON.stringify(args); 63 64 if (cache.has(key)) { 65 return cache.get(key); 66 } 67 68 const result = original.apply(this, args); 69 cache.set(key, result); 70 return result; 71 }; 72 73 return descriptor; 74} 75 76class MathService { 77 @Memoize 78 fibonacci(n: number): number { 79 if (n <= 1) return n; 80 return this.fibonacci(n - 1) + this.fibonacci(n - 2); 81 } 82}

Property Decorators#

1// Validation decorator 2function MinLength(length: number) { 3 return function (target: any, propertyKey: string) { 4 let value: string; 5 6 const getter = () => value; 7 const setter = (newVal: string) => { 8 if (newVal.length < length) { 9 throw new Error( 10 `${propertyKey} must be at least ${length} characters` 11 ); 12 } 13 value = newVal; 14 }; 15 16 Object.defineProperty(target, propertyKey, { 17 get: getter, 18 set: setter, 19 enumerable: true, 20 configurable: true, 21 }); 22 }; 23} 24 25class User { 26 @MinLength(3) 27 name: string; 28 29 @MinLength(8) 30 password: string; 31} 32 33// Observable property 34function Observable(target: any, propertyKey: string) { 35 const privateKey = `_${propertyKey}`; 36 const callbacksKey = `_${propertyKey}Callbacks`; 37 38 Object.defineProperty(target, propertyKey, { 39 get() { 40 return this[privateKey]; 41 }, 42 set(value: any) { 43 const oldValue = this[privateKey]; 44 this[privateKey] = value; 45 46 if (this[callbacksKey]) { 47 this[callbacksKey].forEach((cb: Function) => cb(value, oldValue)); 48 } 49 }, 50 }); 51 52 target.observe = function (prop: string, callback: Function) { 53 const key = `_${prop}Callbacks`; 54 if (!this[key]) { 55 this[key] = []; 56 } 57 this[key].push(callback); 58 }; 59} 60 61class Store { 62 @Observable 63 count: number = 0; 64} 65 66const store = new Store(); 67store.observe('count', (newVal: number) => console.log('Count:', newVal)); 68store.count = 5; // Logs: Count: 5

Parameter Decorators#

1import 'reflect-metadata'; 2 3const requiredMetadataKey = Symbol('required'); 4 5function Required( 6 target: any, 7 propertyKey: string, 8 parameterIndex: number 9) { 10 const existingRequired: number[] = 11 Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; 12 existingRequired.push(parameterIndex); 13 Reflect.defineMetadata( 14 requiredMetadataKey, 15 existingRequired, 16 target, 17 propertyKey 18 ); 19} 20 21function Validate( 22 target: any, 23 propertyKey: string, 24 descriptor: PropertyDescriptor 25) { 26 const method = descriptor.value; 27 28 descriptor.value = function (...args: any[]) { 29 const requiredParams: number[] = 30 Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; 31 32 for (const index of requiredParams) { 33 if (args[index] === undefined || args[index] === null) { 34 throw new Error(`Parameter at index ${index} is required`); 35 } 36 } 37 38 return method.apply(this, args); 39 }; 40} 41 42class UserService { 43 @Validate 44 createUser(@Required name: string, age?: number) { 45 return { name, age }; 46 } 47}

Accessor Decorators#

1function Readonly( 2 target: any, 3 propertyKey: string, 4 descriptor: PropertyDescriptor 5) { 6 descriptor.writable = false; 7 return descriptor; 8} 9 10class Config { 11 private _apiKey: string; 12 13 constructor(apiKey: string) { 14 this._apiKey = apiKey; 15 } 16 17 @Readonly 18 get apiKey() { 19 return this._apiKey; 20 } 21} 22 23// Lazy initialization 24function Lazy( 25 target: any, 26 propertyKey: string, 27 descriptor: PropertyDescriptor 28) { 29 const getter = descriptor.get!; 30 const key = Symbol(`lazy_${propertyKey}`); 31 32 descriptor.get = function () { 33 if (!this[key]) { 34 this[key] = getter.call(this); 35 } 36 return this[key]; 37 }; 38 39 return descriptor; 40} 41 42class ExpensiveService { 43 @Lazy 44 get connection() { 45 console.log('Creating connection...'); 46 return createConnection(); 47 } 48}

Real-World Patterns#

1// Dependency injection 2const Injectable = (): ClassDecorator => { 3 return (target) => { 4 Reflect.defineMetadata('injectable', true, target); 5 }; 6}; 7 8const Inject = (token: string): ParameterDecorator => { 9 return (target, propertyKey, parameterIndex) => { 10 const existingInjections = 11 Reflect.getMetadata('injections', target) || []; 12 existingInjections.push({ index: parameterIndex, token }); 13 Reflect.defineMetadata('injections', existingInjections, target); 14 }; 15}; 16 17@Injectable() 18class UserService { 19 constructor( 20 @Inject('Database') private db: Database, 21 @Inject('Logger') private logger: Logger 22 ) {} 23} 24 25// Route decorators (like Express) 26function Controller(basePath: string): ClassDecorator { 27 return (target) => { 28 Reflect.defineMetadata('basePath', basePath, target); 29 }; 30} 31 32function Get(path: string): MethodDecorator { 33 return (target, propertyKey, descriptor) => { 34 Reflect.defineMetadata('route', { method: 'GET', path }, target, propertyKey); 35 }; 36} 37 38function Post(path: string): MethodDecorator { 39 return (target, propertyKey, descriptor) => { 40 Reflect.defineMetadata('route', { method: 'POST', path }, target, propertyKey); 41 }; 42} 43 44@Controller('/users') 45class UserController { 46 @Get('/') 47 getAll() { 48 return this.userService.findAll(); 49 } 50 51 @Get('/:id') 52 getOne(id: string) { 53 return this.userService.findById(id); 54 } 55 56 @Post('/') 57 create(data: CreateUserDto) { 58 return this.userService.create(data); 59 } 60}

Best Practices#

Design: ✓ Keep decorators focused ✓ Use factories for configuration ✓ Compose multiple decorators ✓ Document decorator behavior Performance: ✓ Avoid heavy computation in decorators ✓ Use lazy initialization ✓ Cache metadata lookups ✓ Consider runtime cost Testing: ✓ Test decorated and undecorated ✓ Mock decorator effects ✓ Test decorator combinations ✓ Verify metadata

Conclusion#

Decorators provide powerful metaprogramming capabilities in TypeScript. Use them for cross-cutting concerns like logging, validation, and dependency injection. Keep decorators simple and composable, and remember they run at class definition time, not instantiation.

Share this article

Help spread the word about Bootspring