← Назад к вопросам

Реализовывал ли Decorator на TypeScript

2.0 Middle🔥 151 комментариев
#TypeScript#Архитектура и паттерны#ООП

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

TypeScript Decorators: практический опыт

Что такое Decorators

Decorator — это функция, которая модифицирует класс, метод, параметр или свойство во время компиляции. Это синтаксический сахар над паттерном Decorator из Design Patterns.

Decorators — это экспериментальная фишка TypeScript, которая станет частью ES стандарта.

Включение поддержки Decorators

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020"
  }
}

1. Method Decorators — логирование вызовов

Это мой самый частый use case. Декоратор логирует все вызовы метода:

// src/decorators/Log.ts
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    console.log(`[${new Date().toISOString()}] Calling ${propertyKey}`);
    console.log('Arguments:', args);
    
    try {
      const result = await originalMethod.apply(this, args);
      console.log('Result:', result);
      return result;
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  };

  return descriptor;
}

export { Log };

Использование:

class UserService {
  @Log
  async getUserById(id: number) {
    return { id, name: 'John' };
  }
}

// При вызове:
// [2024-03-29T10:15:30Z] Calling getUserById
// Arguments: [ 123 ]
// Result: { id: 123, name: 'John' }

2. Class Decorators — с проверкой прав

Декоратор, который проверяет права доступа перед выполнением метода:

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

    descriptor.value = async function (req: Request, ...args: any[]) {
      const userRole = req.user?.role;
      
      if (!userRole || !roles.includes(userRole)) {
        throw new Error(`Access denied. Required roles: ${roles.join(', ')}`);
      }

      return originalMethod.apply(this, [req, ...args]);
    };

    return descriptor;
  };
}

// Использование
class AdminController {
  @Authenticate(['admin', 'moderator'])
  async deleteUser(req: Request, userId: number) {
    return { deleted: true, userId };
  }
}

3. Кэширование результатов

Декоратор, который кэширует результаты метода:

function Cacheable(ttl: number = 3600) {
  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 cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
      const cached = cache.get(cacheKey);

      // Проверяем, если кэш свежий
      if (cached && Date.now() - cached.timestamp < ttl * 1000) {
        console.log(`Cache hit for ${cacheKey}`);
        return cached.value;
      }

      // Иначе вычисляем и кэшируем
      const result = await originalMethod.apply(this, args);
      cache.set(cacheKey, { value: result, timestamp: Date.now() });
      return result;
    };

    return descriptor;
  };
}

// Использование
class ProductService {
  @Cacheable(3600) // Кэш на 1 час
  async getProductById(id: number) {
    // Дорогой запрос в БД
    return await db.query('SELECT * FROM products WHERE id = $1', [id]);
  }
}

4. Валидация параметров

Декоратор, который валидирует входные параметры:

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

    descriptor.value = async function (data: any, ...args: any[]) {
      try {
        // Используем Zod или Joi для валидации
        const validated = await schema.parseAsync(data);
        return originalMethod.apply(this, [validated, ...args]);
      } catch (error) {
        throw new Error(`Validation error: ${error.message}`);
      }
    };

    return descriptor;
  };
}

// Использование с Zod
import { z } from 'zod';

const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(1)
});

class UserService {
  @Validate(createUserSchema)
  async createUser(data: any) {
    // data уже валидирован
    return { id: 1, ...data };
  }
}

5. Retry декоратор — для нестабильных операций

Декоратор, который повторяет вызов в случае ошибки:

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[]) {
      let lastError: Error;

      for (let i = 0; i < attempts; i++) {
        try {
          console.log(`Attempt ${i + 1} of ${attempts}`);
          return await originalMethod.apply(this, args);
        } catch (error) {
          lastError = error;
          if (i < attempts - 1) {
            console.log(`Waiting ${delay}ms before retry...`);
            await new Promise(resolve => setTimeout(resolve, delay));
          }
        }
      }

      throw lastError;
    };

    return descriptor;
  };
}

// Использование
class ExternalApiService {
  @Retry(3, 2000) // 3 попытки с интервалом 2 сек
  async callUnstableApi() {
    return await fetch('https://api.example.com/data');
  }
}

6. Property Decorators — для валидации свойств

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

    const getter = () => value;
    const setter = (newValue: number) => {
      if (newValue < min) {
        throw new Error(`${propertyKey} must be >= ${min}`);
      }
      value = newValue;
    };

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

// Использование
class Product {
  @Min(0)
  price: number;
}

const p = new Product();
p.price = -10; // Ошибка!

Реальный пример: комбинация декораторов

class PaymentService {
  @Log
  @Authenticate(['admin'])
  @Cacheable(300) // кэш на 5 минут для админов
  @Retry(3, 1000) // повторяем 3 раза при ошибке
  async processPayment(paymentData: any) {
    return await paymentGateway.charge(paymentData);
  }
}

Порядок выполнения:

  1. Log логирует вызов
  2. Authenticate проверяет права
  3. Cacheable проверяет кэш
  4. Retry обёртывает логику повторов
  5. Основной метод

Где я использую Decorators в production

  • NestJS — весь фреймворк построен на декораторах
  • API контроллеры — @Get(), @Post(), @Authenticate()
  • Сервисы — @Cacheable(), @Log(), @Retry()
  • Middleware — @UseMiddleware(), @UseGuards()

Плюсы и минусы

Плюсы:

  • Чистый и читаемый код
  • Разделение concerns (логирование отдельно от бизнес-логики)
  • Легко переиспользовать
  • Работает с TypeScript

Минусы:

  • Экспериментальная фишка (может измениться)
  • Overhead в runtime
  • Может быть сложным для новичков
  • Нужно правильно понимать порядок применения