TypeScript Декораторы

Декораторы — мощная возможность TypeScript, позволяющая добавлять метаданные и модифицировать классы и их члены на этапе проектирования.

Они широко применяются во фреймворках вроде Angular и NestJS для внедрения зависимостей, маршрутизации и других задач.


Включение декораторов

Чтобы использовать декораторы в TypeScript, их нужно активировать в файле tsconfig.json:

tsconfig.json


{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictPropertyInitialization": false
  },
  "include": ["src/**/*.ts"]
}

Примечание: Опция emitDecoratorMetadata включает экспериментальную поддержку генерации метаданных типов для декораторов — это используется в библиотеках вроде TypeORM и валидаторах классов.


Типы декораторов

TypeScript поддерживает несколько типов декораторов, которые можно применять к разным объявлениям:

Тип декоратораПрименяется кСигнатура
Декоратор классаОбъявлениям классов(constructor: Function) => void
Декоратор методаМетодам класса(target: any, propertyKey: string, descriptor: PropertyDescriptor) => void
Декоратор свойстваСвойствам класса(target: any, propertyKey: string) => void
Декоратор параметраПараметрам методов(target: any, propertyKey: string, parameterIndex: number) => void

Декораторы классов

Декораторы классов применяются к конструктору класса и позволяют наблюдать за определением класса, модифицировать его или заменять.

Они вызываются при объявлении класса — не при создании экземпляров.

Базовый декоратор класса

Этот простой декоратор логирует момент определения класса:


// Простой декоратор класса, логирующий его определение
function logClass(constructor: Function) {
  console.log(`Класс ${constructor.name} определён в ${new Date().toISOString()}`);
}

// Применение декоратора
@logClass
class UserService {
  getUsers() {
    return ['Alice', 'Bob', 'Charlie'];
  }
}

// Вывод при загрузке файла: "Класс UserService определён в [timestamp]"

Декоратор класса с модификацией конструктора

Пример, показывающий, как модифицировать класс, добавляя свойства и методы:


// Декоратор, добавляющий свойство версии и логирующий создание экземпляра
function versioned(version: string) {
  return function (constructor: Function) {
    // Добавляем статическое свойство
    constructor.prototype.version = version;
    
    // Сохраняем оригинальный конструктор
    const original = constructor;
    
    // Создаём новый конструктор, оборачивающий оригинальный
    const newConstructor: any = function (...args: any[]) {
      console.log(`Создаётся экземпляр ${original.name} v${version}`);
      return new original(...args);
    };
    
    // Копируем прототип, чтобы работал instanceof
    newConstructor.prototype = original.prototype;
    return newConstructor;
  };
}

// Применяем декоратор с версией
@versioned('1.0.0')
class ApiClient {
  fetchData() {
    console.log('Загружаем данные...');
  }
}

const client = new ApiClient();
console.log((client as any).version); // Выводит: 1.0.0
client.fetchData();

Декоратор "запечатанного" класса

Этот декоратор запрещает добавление новых свойств в класс и помечает все существующие свойства как неконфигурируемые:


function sealed(constructor: Function) {
  console.log(`Запечатываем ${constructor.name}...`);
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `Привет, ${this.greeting}`;
  }
}

// В строгом режиме это вызовет ошибку
// Greeter.prototype.newMethod = function() {}; // Ошибка: нельзя добавить свойство newMethod

Ключевые моменты о декораторах классов

  • Декораторы классов вызываются при объявлении класса, а не при создании экземпляров.
  • Они получают конструктор класса как единственный параметр.
  • Могут возвращать новую функцию‑конструктор, заменяющую оригинальный класс.
  • Выполняются снизу вверх (сначала самый внутренний декоратор).
  • Используются для логирования, запечатывания, замораживания или добавления метаданных.

Декораторы методов

Декораторы методов применяются к объявлениям методов и позволяют наблюдать за ними, модифицировать или заменять их определения.

Они получают три параметра:

  1. target: Прототип класса (для методов экземпляра) или функция‑конструктор (для статических методов)
  2. propertyKey: Имя метода
  3. descriptor: Дескриптор свойства для метода

Декоратор измерения времени выполнения метода

Этот декоратор замеряет и логирует время выполнения метода:


// Декоратор метода для измерения времени выполнения
function measureTime(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} выполнен за ${(end - start).toFixed(2)} мс`);
    return result;
  };
  return descriptor;
}

// Используем декоратор
class DataProcessor {
  @measureTime
  processData(data: number[]): number[] {
    // Имитируем время обработки
    for (let i = 0; i < 100000000; i++) { /* обработка */ }
    return data.map(x => x * 2);
  }
}

// При вызове будет выведено время выполнения
const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);

Декоратор контроля доступа по ролям

Пример реализации контроля доступа на основе ролей с помощью декораторов методов:


// Роли пользователей
type UserRole = 'admin' | 'editor' | 'viewer';

// Контекст текущего пользователя (упрощённо)
const currentUser = {
  id: 1,
  name: 'John Doe',
  roles: ['viewer'] as UserRole[]
};

// Фабрика декораторов для контроля доступа по ролям
function AllowedRoles(...allowedRoles: UserRole[]) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const hasPermission = allowedRoles.some(role =>
        currentUser.roles.includes(role)
      );
      if (!hasPermission) {
        throw new Error(
          `Пользователь ${currentUser.name} не авторизован для вызова ${propertyKey}`
        );
      }
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

// Используем декоратор
class DocumentService {
  @AllowedRoles('admin', 'editor')
  deleteDocument(id: string) {
    console.log(`Документ ${id} удалён`);
  }
  
  @AllowedRoles('admin', 'editor', 'viewer')
  viewDocument(id: string) {
    console.log(`Просмотр документа ${id}`);
  }
}

// Использование
const docService = new DocumentService();
try {
  docService.viewDocument('doc123'); // Работает — роль viewer разрешена
  docService.deleteDocument('doc123'); // Выбрасывает ошибку — viewer не может удалять
} catch (error) {
  console.error(error.message);
}

// Меняем роль пользователя на admin
currentUser.roles = ['admin'];
docService.deleteDocument('doc123'); // Теперь работает — admin может удалять

Декоратор предупреждения об устаревании

Этот декоратор добавляет предупреждение об устаревании метода, который будет удалён в будущей версии:


function deprecated(message: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.warn(`Предупреждение: метод ${propertyKey} устарел. ${message}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class PaymentService {
  @deprecated('Используйте processPaymentV2 вместо этого')
  processPayment(amount: number, currency: string) {
    console.log(`Обработка платежа на сумму ${amount} ${currency}`);
  }

  processPaymentV2(amount: number, currency: string) {
    console.log(`Обработка платежа v2 на сумму ${amount} ${currency}`);
  }
}

const payment = new PaymentService();
payment.processPayment(100, 'USD'); // Показывает предупреждение об устаревании
payment.processPaymentV2(100, 'USD'); // Предупреждения нет

Ключевые моменты о декораторах методов

  • Декораторы методов вызываются при определении метода, а не при его вызове.
  • Они могут изменять поведение метода, оборачивая его дополнительной логикой.
  • Используются для сквозных задач: логирования, валидации, авторизации.
  • Получают дескриптор свойства метода — это позволяет модифицировать его поведение.
  • Должны возвращать дескриптор свойства или undefined (если не изменяют дескриптор).

Декораторы свойств

Декораторы свойств применяются к объявлениям свойств и позволяют наблюдать за ними, модифицировать или заменять их определения.

Они получают два параметра:

  1. target: Прототип класса (для свойств экземпляра) или функция‑конструктор (для статических свойств)
  2. propertyKey: Имя свойства

Декоратор форматирования свойства

Этот декоратор автоматически форматирует свойство при его установке:


// Декоратор свойства для форматирования строкового значения
function format(formatString: string) {
  return function (target: any, propertyKey: string) {
    let value: string;
    const getter = () => value;
    const setter = (newVal: string) => {
      value = formatString.replace('{}', newVal);
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Greeter {
  @format('Hello, {}!')
  greeting: string;
}

const greeter = new Greeter();
greeter.greeting = 'World';
console.log(greeter.greeting); // Выводит: Hello, World!

Декоратор логирования доступа к свойству

Этот декоратор логирует доступ к свойству и его изменения:


function logProperty(target: any, propertyKey: string) {
  let value: any;
  const getter = function() {
    console.log(`Получаем ${propertyKey}: ${value}`);
    return value;
  };

  const setter = function(newVal: any) {
    console.log(`Устанавливаем ${propertyKey} со значения ${value} на ${newVal}`);
    value = newVal;
  };

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

class Product {
  @logProperty
  name: string;
  @logProperty
  price: number;

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

const product = new Product('Laptop', 999.99);
product.price = 899.99; // Логирует: Устанавливаем price со значения 999.99 на 899.99
console.log(product.name); // Логирует: Получаем name: Laptop

Декоратор обязательного свойства

Этот декоратор гарантирует, что свойство обязательно должно быть установлено:


function required(target: any, propertyKey: string) {
  let value: any;

  const getter = function() {
    if (value === undefined) {
      throw new Error(`Свойство ${propertyKey} обязательно к заполнению`);
    }
    return value;
  };

  const setter = function(newVal: any) {
    value = newVal;
  };

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

class User {
  @required
  username: string;
  @required
  email: string;
  age?: number;

  constructor(username: string, email: string) {
    this.username = username;
    this.email = email;
  }
}

const user1 = new User('johndoe', 'john@example.com'); // Работает
// const user2 = new User(undefined, 'test@example.com'); // Ошибка: Свойство username обязательно
// const user2 = new User('johndoe', undefined); // Ошибка: Свойство email обязательно

Ключевые моменты о декораторах свойств

  • Декораторы свойств вызываются при определении свойства, а не при его использовании.
  • В отличие от декораторов методов, они не получают дескриптор свойства.
  • Для изменения поведения свойства нужно использовать Object.defineProperty.
  • Часто применяются для отражения метаданных или модификации доступа к свойствам.
  • Могут комбинироваться с другими декораторами для более сложных сценариев.

Декораторы параметров

Декораторы параметров применяются к объявлениям параметров в конструкторе или методе.

Они получают три параметра:

  1. target: Прототип класса (для методов экземпляра) или функция‑конструктор (для статических методов)
  2. propertyKey: Имя метода (или undefined для параметров конструктора)
  3. parameterIndex: Порядковый индекс параметра в списке параметров функции

Декоратор валидации параметров

Этот декоратор проверяет параметры метода:


function validateParam(type: 'string' | 'number' | 'boolean') {
  return function (target: any, propertyKey: string | symbol, parameterIndex: number) {
    const existingValidations: any[] = Reflect.getOwnMetadata('validations', target, propertyKey) || [];
    existingValidations.push({ index: parameterIndex, type });
    Reflect.defineMetadata('validations', existingValidations, target, propertyKey);
  };
}

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const validations: Array<{index: number, type: string}> =
      Reflect.getOwnMetadata('validations', target, propertyKey) || [];

    for (const validation of validations) {
      const { index, type } = validation;
      const param = args[index];
      let isValid = false;

      switch (type) {
        case 'string':
          isValid = typeof param === 'string' && param.length > 0;
          break;
        case 'number':
          isValid = typeof param === 'number' && !isNaN(param);
          break;
        case 'boolean':
          isValid = typeof param === 'boolean';
      }

      if (!isValid) {
        throw new Error(`Параметр под индексом ${index} не прошёл валидацию типа ${type}`);
      }
    }

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

class UserService {
  @validate
  createUser(
    @validateParam('string') name: string,
    @validateParam('number') age: number,
    @validateParam('boolean') isActive: boolean
  ) {
    console.log(`Создаём пользователя: ${name}, ${age}, ${isActive}`);
  }
}

const service = new UserService();
service.createUser('John', 30, true); // Работает
// service.createUser('', 30, true); // Ошибка: Параметр под индексом 0 не прошёл валидацию строки
// service.createUser('John', 'thirty', true); // Ошибка: Параметр под индексом 1 не прошёл валидацию числа

Ключевые моменты о декораторах параметров

  • Декораторы параметров вызываются при определении метода, а не при его вызове.
  • Часто применяются совместно с декораторами методов для реализации сквозных задач.
  • Могут использоваться вместе с библиотекой reflect-metadata для сохранения и извлечения метаданных.
  • Широко распространены во фреймворках внедрения зависимостей.
  • Получают индекс параметра — это позволяет обращаться к значению параметра во время выполнения.

Фабрики декораторов

Фабрики декораторов — это функции, возвращающие функцию‑декоратор.

Они позволяют настраивать декораторы через передачу параметров, что делает их более гибкими и пригодными для повторного использования.

Настраиваемый декоратор логирования

Пример создания настраиваемого декоратора логирования с возможностью выбора уровня логирования:


// Фабрика декораторов с поддержкой конфигурации
function logWithConfig(config: { level: 'log' | 'warn' | 'error', message?: string }) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const { level = 'log', message = 'Выполнение метода' } = config;
      console[level](`${message}: ${propertyKey}`, { arguments: args });
      const result = originalMethod.apply(this, args);
      console[level](`${propertyKey} завершён`);
      return result;
    };
    return descriptor;
  };
}

class PaymentService {
  @logWithConfig({ level: 'log', message: 'Обработка платежа' })
  processPayment(amount: number) {
    console.log(`Обработка платежа на сумму $${amount}`);
  }
}

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

Пример, демонстрирующий порядок выполнения при применении нескольких декораторов:

Когда к объявлению применяется несколько декораторов, они выполняются в следующем порядке:

  1. Декораторы параметров → затем декораторы методов/аксессоров/свойств — для каждого члена экземпляра.
  2. Декораторы параметров → затем декораторы методов/аксессоров/свойств — для каждого статического члена.
  3. Декораторы параметров для конструктора.
  4. Декораторы классов для класса.

function first() {
  console.log('first(): фабрика отработала');
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('first(): вызван');
  };
}

function second() {
  console.log('second(): фабрика отработала');
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('second(): вызван');
  };
}

class ExampleClass {
  @first()
  @second()
  method() {}
}

// Вывод:
// second(): фабрика отработала
// first(): фабрика отработала
// first(): вызван
// second(): вызван


Реальные примеры

API‑контроллер с декораторами

Пример использования декораторов для создания простого API‑контроллера (по аналогии с NestJS или Express):


// Упрощённые реализации декораторов (для примера)
const ROUTES: any[] = [];

function Controller(prefix: string = '') {
  return function (constructor: Function) {
    constructor.prototype.prefix = prefix;
  };
}

function Get(path: string = '') {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    ROUTES.push({
      method: 'get',
      path,
      handler: descriptor.value,
      target: target.constructor
    });
  };
}

// Использование декораторов
@Controller('/users')
class UserController {
  @Get('/')
  getAllUsers() {
    return { users: [{ id: 1, name: 'John' }] };
  }

  @Get('/:id')
  getUserById(id: string) {
    return { id, name: 'John' };
  }
}

// Имитация регистрации маршрутов
function registerRoutes() {
  ROUTES.forEach(route => {
    const prefix = route.target.prototype.prefix || '';
    console.log(`Зарегистрирован ${route.method.toUpperCase()} ${prefix}${route.path}`);
  });
}

registerRoutes();
// Вывод:
// Зарегистрирован GET /users
// Зарегистрирован GET /users/:id


Лучшие практики

Соблюдайте следующие правила при работе с декораторами:

  1. Фокусируйтесь на одной задаче: Каждый декоратор должен выполнять одну задачу.
  2. Документируйте поведение: Чётко описывайте, что делает декоратор и какие побочные эффекты возможны.
  3. Используйте фабрики декораторов: Используйте фабрики декораторов для их настройки — это повышает повторное использование декораторов.
  4. Производительность: Учитывайте воздействие на производительность, особенно в критичных по скорости работы участках кода.
  5. Типобезопасность: По возможности используйте систему типов TypeScript для проверки декораторов.
  6. Обработка ошибок: Реализуйте корректную обработку исключений внутри декораторов.
  7. Тестирование: Пишите юнит‑тесты для декораторов, чтобы убедиться в их корректной работе.
  8. Метаданные: Применяйте библиотеку reflect-metadata в сложных сценариях, требующих информации о типах во время выполнения.

Типичные ошибки

Избегайте следующих ошибок:

  • Не включена поддержка декораторов: Убедитесь, что в tsconfig.json задана опция experimentalDecorators: true.
  • Неверная сигнатура декоратора: У каждого типа декораторов своя сигнатура — ошибки в параметрах приводят к сбоям во время выполнения.
  • Порядок выполнения: Декораторы выполняются снизу вверх для каждого объявления.
  • Инициализация свойств: Декораторы свойств выполняются до инициализации свойств экземпляра.
  • Отображение метаданных: Не забудьте импортировать библиотеку reflect-metadata, если используете метаданные декораторов.
  • Нагрузка на производительность: Будьте осторожны с декораторами, добавляющими значительные задержки в коде, для которого критична скорость выполнения.
  • Совместимость с браузерами: Декораторы — предложение стадии 3, могут требовать транспиляции для старых браузеров.

Пример: декоратор свойства


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

class Person {
  @readonly
  name = "John";
}

Пример: декоратор параметров


function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`Параметр в методе ${propertyKey} под индексом ${parameterIndex}`);
}

class Demo {
  greet(@logParameter message: string) {
    return message;
  }
}

Чтобы активировать декораторы, добавьте в tsconfig.json следующую настройку:

Пример


{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Где используются декораторы?

  • Angular: для компонентов, сервисов, модулей и т. д.
  • NestJS: для контроллеров, провайдеров, маршрутов и т. д.