Mastering TypeScript Decorators

Last Modified: January 6, 2025

TypeScript decorators provide powerful metaprogramming capabilities for modern web applications. Let's explore how to create and use decorators effectively for better code organization and reusability.

Understanding Decorators 🎯

Decorators enable:

  • Metaprogramming capabilities
  • Code reusability
  • Aspect-oriented programming
  • Runtime modifications
  • Clean architecture patterns

Basic Decorator Implementation

Create class decorators:

function Logger(prefix: string) {
    return function (target: any) {
        // Store original methods
        const original = target.prototype;

        // Add logging to all methods
        for (const key of Object.getOwnPropertyNames(original)) {
            if (key !== 'constructor') {
                const descriptor = Object.getOwnPropertyDescriptor(
                    original,
                    key
                );

                if (descriptor && typeof descriptor.value === 'function') {
                    Object.defineProperty(original, key, {
                        value: function (...args: any[]) {
                            console.log(
                                `${prefix} Calling ${key} with:`,
                                args
                            );
                            return descriptor.value.apply(this, args);
                        }
                    });
                }
            }
        }
    };
}

@Logger('MyClass')
class MyClass {
    doSomething(value: string) {
        return `Processed: ${value}`;
    }
}

Method Decorators

Implement method-level decorators:

function Measure() {
    return function (
        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);
                return result;
            } finally {
                const end = performance.now();
                console.log(
                    `${propertyKey} took ${end - start}ms`
                );
            }
        };

        return descriptor;
    };
}

class ApiService {
    @Measure()
    async fetchData(id: string) {
        // Simulate API call
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { id, data: 'Some data' };
    }
}

Property Decorators

Create property decorators:

function ValidateLength(min: number, max: number) {
    return function (target: any, propertyKey: string) {
        let value: string;

        const getter = function () {
            return value;
        };

        const setter = function (newVal: string) {
            if (newVal.length < min || newVal.length > max) {
                throw new Error(
                    `${propertyKey} must be between ` +
                    `${min} and ${max} characters`
                );
            }
            value = newVal;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter
        });
    };
}

class User {
    @ValidateLength(3, 50)
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

Parameter Decorators

Implement parameter validation:

function Required() {
    return function (
        target: any,
        propertyKey: string,
        parameterIndex: number
    ) {
        const requiredParams = Reflect.getMetadata(
            'required',
            target,
            propertyKey
        ) || [];

        requiredParams.push(parameterIndex);

        Reflect.defineMetadata(
            'required',
            requiredParams,
            target,
            propertyKey
        );
    };
}

class UserService {
    createUser(@Required() name: string, age?: number) {
        return { name, age };
    }
}

Advanced Patterns

Dependency Injection

Implement DI decorators:

const container = new Map();

function Injectable() {
    return function (target: any) {
        container.set(target.name, new target());
    };
}

function Inject(token: string) {
    return function (
        target: any,
        propertyKey: string
    ) {
        Object.defineProperty(target, propertyKey, {
            get: () => container.get(token)
        });
    };
}

@Injectable()
class UserRepository {
    findById(id: string) {
        return { id, name: 'John' };
    }
}

@Injectable()
class UserController {
    @Inject('UserRepository')
    private userRepository: UserRepository;

    getUser(id: string) {
        return this.userRepository.findById(id);
    }
}

Caching Decorator

Implement method caching:

function Cacheable(ttl: number = 60000) {
    const cache = new Map<string, {
        value: any;
        timestamp: number;
    }>();

    return function (
        target: any,
        propertyKey: string,
        descriptor: PropertyDescriptor
    ) {
        const originalMethod = descriptor.value;

        descriptor.value = async function (...args: any[]) {
            const key = `${propertyKey}-${JSON.stringify(args)}`;
            const cached = cache.get(key);

            if (cached && Date.now() - cached.timestamp < ttl) {
                return cached.value;
            }

            const result = await originalMethod.apply(this, args);

            cache.set(key, {
                value: result,
                timestamp: Date.now()
            });

            return result;
        };

        return descriptor;
    };
}

Validation Decorators

Create validation decorators:

function Validate(schema: any) {
    return function (
        target: any,
        propertyKey: string,
        descriptor: PropertyDescriptor
    ) {
        const originalMethod = descriptor.value;

        descriptor.value = function (...args: any[]) {
            const validation = schema.validate(args[0]);

            if (validation.error) {
                throw new Error(validation.error.message);
            }

            return originalMethod.apply(this, args);
        };

        return descriptor;
    };
}

const userSchema = {
    validate: (data: any) => {
        if (!data.name || data.name.length < 3) {
            return {
                error: { message: 'Invalid name' }
            };
        }
        return { error: null };
    }
};

class UserService {
    @Validate(userSchema)
    createUser(userData: any) {
        return userData;
    }
}

Best Practices

  1. Decorator Design
  • Keep decorators focused
  • Use meaningful names
  • Document behavior
  • Handle errors properly
  1. Performance
  • Minimize runtime overhead
  • Cache computed values
  • Optimize metadata usage
  • Monitor impact
  1. Maintainability
  • Follow single responsibility
  • Use composition
  • Document dependencies
  • Test thoroughly
  1. Development
  • Use TypeScript strict mode
  • Implement proper typing
  • Handle edge cases
  • Monitor decorator usage

Testing Decorators

Implement decorator tests:

describe('Measure Decorator', () => {
    it('should measure execution time', async () => {
        const spy = jest.spyOn(console, 'log');

        class TestClass {
            @Measure()
            async testMethod() {
                await new Promise(
                    resolve => setTimeout(resolve, 100)
                );
            }
        }

        const instance = new TestClass();
        await instance.testMethod();

        expect(spy).toHaveBeenCalled();
        expect(spy.mock.calls[0][0]).toMatch(
            /testMethod took \d+ms/
        );
    });
});

Conclusion

TypeScript decorators provide powerful tools for metaprogramming and code organization. Remember to:

  • Design focused decorators
  • Consider performance impact
  • Test thoroughly
  • Document behavior
  • Handle errors properly
  • Follow best practices

As decorators become more widely used, these patterns will become essential for modern TypeScript development.