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); // DateMethod 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: 5Parameter 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.