TypeScript Decorators | Practical Guide to Class & Method Decorators

TypeScript Decorators | Practical Guide to Class & Method Decorators

이 글의 핵심

Hands-on TypeScript decorators: what they are, how to configure the compiler, and how to use class, method, property, and parameter decorators with factories for cross-cutting concerns.

Introduction

What are decorators?

A decorator is a special declaration that adds metadata to classes, methods, properties, and more—or wraps their behavior.


1. Configuration

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

2. Class decorators

Basic usage

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class User {
    name: string;
    
    constructor(name: string) {
        this.name = name;
    }
}

Example: logging

function logger<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        constructor(...args: any[]) {
            super(...args);
            console.log(`${constructor.name} 인스턴스 생성됨`);
        }
    };
}

@logger
class User {
    constructor(public name: string) {}
}

const user = new User("홍길동");
// Output: User 인스턴스 생성됨

3. Method decorators

Basic usage

function log(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
        console.log(`${propertyKey} 호출됨:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`${propertyKey} 결과:`, result);
        return result;
    };
    
    return descriptor;
}

class Calculator {
    @log
    add(a: number, b: number): number {
        return a + b;
    }
}

const calc = new Calculator();
calc.add(10, 20);
// Output:
// add 호출됨: [10, 20]
// add 결과: 30

Example: timing

function measure(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args: any[]) {
        const start = performance.now();
        const result = await originalMethod.apply(this, args);
        const end = performance.now();
        console.log(`${propertyKey} 실행 시간: ${(end - start).toFixed(2)}ms`);
        return result;
    };
    
    return descriptor;
}

class DataService {
    @measure
    async fetchData() {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { data: "결과" };
    }
}

const service = new DataService();
await service.fetchData();
// Output: fetchData 실행 시간: 1001.23ms

4. Property decorators

Basic usage

function readonly(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
        writable: false
    });
}

class User {
    @readonly
    id: string = "U001";
    
    name: string = "홍길동";
}

const user = new User();
console.log(user.id);  // U001
// user.id = "U002";   // Error (strict mode)

Example: validation

function validate(validationFn: (value: any) => boolean) {
    return function(target: any, propertyKey: string) {
        let value: any;
        
        Object.defineProperty(target, propertyKey, {
            get() {
                return value;
            },
            set(newValue: any) {
                if (!validationFn(newValue)) {
                    throw new Error(`${propertyKey} 검증 실패`);
                }
                value = newValue;
            }
        });
    };
}

class User {
    @validate((value) => value.length >= 2)
    name!: string;
    
    @validate((value) => value >= 0 && value <= 150)
    age!: number;
}

const user = new User();
user.name = "홍길동";  // ✅
user.age = 25;        // ✅

// user.name = "a";   // ❌ Error
// user.age = 200;    // ❌ Error

5. Parameter decorators

Basic usage

function required(
    target: any,
    propertyKey: string,
    parameterIndex: number
) {
    console.log(`${propertyKey}의 ${parameterIndex}번째 매개변수는 필수입니다`);
}

class User {
    greet(@required name: string) {
        console.log(`안녕하세요, ${name}님!`);
    }
}

6. Decorator factories

Concept

Factories return a decorator so you can pass configuration.

function log(prefix: string) {
    return function(
        target: any,
        propertyKey: string,
        descriptor: PropertyDescriptor
    ) {
        const originalMethod = descriptor.value;
        
        descriptor.value = function(...args: any[]) {
            console.log(`[${prefix}] ${propertyKey} 호출됨`);
            return originalMethod.apply(this, args);
        };
        
        return descriptor;
    };
}

class UserService {
    @log("USER")
    createUser(name: string) {
        console.log(`사용자 생성: ${name}`);
    }
    
    @log("AUTH")
    login(email: string) {
        console.log(`로그인: ${email}`);
    }
}

const service = new UserService();
service.createUser("홍길동");
// Output:
// [USER] createUser 호출됨
// 사용자 생성: 홍길동

service.login("[email protected]");
// Output:
// [AUTH] login 호출됨
// 로그인: [email protected]

7. Practical examples

Example 1: Authorization

function authorize(roles: string[]) {
    return function(
        target: any,
        propertyKey: string,
        descriptor: PropertyDescriptor
    ) {
        const originalMethod = descriptor.value;
        
        descriptor.value = function(...args: any[]) {
            const userRole = getCurrentUserRole();  // e.g. from session
            
            if (!roles.includes(userRole)) {
                throw new Error("Forbidden");
            }
            
            return originalMethod.apply(this, args);
        };
        
        return descriptor;
    };
}

function getCurrentUserRole(): string {
    return "admin";  // In real apps, read from session/JWT
}

class AdminService {
    @authorize(["admin"])
    deleteUser(id: string) {
        console.log(`사용자 삭제: ${id}`);
    }
    
    @authorize(["admin", "moderator"])
    banUser(id: string) {
        console.log(`사용자 차단: ${id}`);
    }
}

const service = new AdminService();
service.deleteUser("U001");  // ✅ succeeds as admin

Example 2: Caching

function cache(ttl: number = 60000) {
    const cacheStore = new Map<string, { value: any; expiry: 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 = cacheStore.get(key);
            
            if (cached && Date.now() < cached.expiry) {
                console.log("캐시에서 반환");
                return cached.value;
            }
            
            console.log("새로 계산");
            const result = await originalMethod.apply(this, args);
            cacheStore.set(key, { value: result, expiry: Date.now() + ttl });
            return result;
        };
        
        return descriptor;
    };
}

class DataService {
    @cache(5000)  // 5 second TTL
    async fetchUser(id: string) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { id, name: "홍길동" };
    }
}

const service = new DataService();

await service.fetchUser("U001");  // 새로 계산
await service.fetchUser("U001");  // 캐시에서 반환

Summary

Takeaways

  1. Class decorators: wrap or modify the constructor
  2. Method decorators: wrap method calls
  3. Property decorators: attach behavior to properties
  4. Parameter decorators: record parameter metadata (often with reflection libraries)
  5. Decorator factories: return configured decorators

Next steps