Декораторы и метаданные привносят в TypeScript уровень рефлексии, который делает код гибче и выразительнее. В этой статье объясню, как это работает на практике, какие возможности открываются, и какие ограничения стоит учитывать при внедрении таких техник в реальный проект.

Коротко о декораторах: что и зачем

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

Типы декораторов по месту применения просты: класс, метод, аксессор, свойство и параметр. Синтаксис привычен — перед объявлением ставится знак @ и имя декоратора. Для передачи параметров используют фабрики — функции, возвращающие сам декоратор.

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Call ${propertyKey}`, args);
    return original.apply(this, args);
  };
}
class Service {
  @Log
  fetch(id: number) { /* ... */ }
}

Метаданные и reflect-metadata: как получить информацию о типах

Чтобы декораторы могли опираться на данные о типах, нужно включить две опции в tsconfig: experimentalDecorators и emitDecoratorMetadata. Первая включает возможность использования декораторов, вторая заставляет компилятор генерировать метаданные типов, которые потом считываются через Reflect API.

Библиотека reflect-metadata добавляет глобальный объект Reflect с методами getMetadata и defineMetadata. С её помощью можно читать параметры конструктора, типы свойств и возвращаемые типы методов, если компилятор успел их описать.

Ключ метаданных Что хранит
design:type Тип свойства или возвращаемый тип метода
design:paramtypes Массив типов параметров конструктора или метода
design:returntype Тип, который возвращает метод

Важно понимать: метаданные — это не магия. Компилятор записывает простой RTTI, который не сохраняет информацию о дженериках и сложных типах. Зачастую вы увидите в metadata просто Function или Object для сложных случаев.

Практика: примеры полезных декораторов

Разберу несколько практических сценариев, которые чаще всего встречались в моих проектах. Каждый пример будет коротким, но поясняющим суть применения метаданных.

1) Логирование вызовов уже показано выше — полезно при отладке и для локального анализа. Такой декоратор легко включается и выключается, не меняя тело метода.

function Injectable(): ClassDecorator {
  return (target: any) => {
    Reflect.defineMetadata('injectable', true, target);
  };
}

function Inject(token: any): ParameterDecorator {
  return (target, propertyKey, parameterIndex) => {
    const existing = Reflect.getMetadata('design:paramtypes', target, propertyKey) || [];
    Reflect.defineMetadata(`inject:${parameterIndex}`, token, target, propertyKey);
  };
}

2) Внедрение зависимостей — классический кейс. Сохраняем информацию о типах параметров конструктора и на её основе разрешаем зависимости из контейнера. Это позволяет реализовать простой DI без громоздких конфигураций.

3) Валидация свойств через декораторы удобна для DTO. Метаданные помогают понять тип свойства и автоматически приводить или проверять значение при присвоении.

Ограничения: чего ожидать и чего не ожидать

Метаданные дают полезную информацию, но не дают полную картину типов. Дженерики, условные типы и объединения теряются при компиляции. Часто дизайн:paramtypes вернёт Object вместо конкретного интерфейса.

Ещё один важный момент — порядок инициализации. Декораторы выполняются при объявлении класса, а не при первом использовании. Это значит, что побочные эффекты в декораторах будут происходить на этапе загрузки модуля.

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

Лучшие практики при работе с декораторами и метаданными

Ниже — несколько правил, которые упростят жизнь и помогут избежать типичных ошибок.

  • Включайте emitDecoratorMetadata только если действительно используете метаданные — это может увеличить объём сгенерированного кода.
  • Делайте декораторы минимально инвазивными: они должны расширять поведение, а не заменять логику полностью.
  • Тестируйте декораторы отдельно: мокайте Reflect, чтобы проверить чтение и запись метаданных.
  • Не полагайтесь на метаданные для критичной логики валидации типов — используйте явные проверки при выполнении.

Примеры кода с чтением метаданных

Ниже простой пример, который читает типы параметров конструктора и создаёт экземпляры через контейнер. Это демонстрирует, как метаданные облегчают работу DI-контейнера.

import 'reflect-metadata';

function createInstance(ctor: new (...args: any[]) => T) {
  const types = Reflect.getMetadata('design:paramtypes', ctor) || [];
  const params = types.map((Type: any) => new Type());
  return new ctor(...params);
}

class Repo { }
class Service {
  constructor(private repo: Repo) {}
}
const s = createInstance(Service);

Обратите внимание: если Repo требует параметров, автоматическое создание сломается. Поэтому контейнеры часто умеют резолвить зависимости рекурсивно и поддерживают кастомные фабрики.

Безопасность и поддерживаемость

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

Также учитывайте производительность: массовое чтение и запись метаданных при загрузке модулей может повлиять на время старта приложения. Особенно это заметно в серверных приложениях с большим числом модулей.

Мой опыт: когда декораторы действительно помогают

В нескольких проектах я использовал декораторы для авторизации и для автоматической регистрации обработчиков событий. Это уменьшало шаблонный код и делало контроллеры компактнее. Однако решение добавлять эту технику принималось не сразу — сначала оценивали, насколько прозрачным остаётся поведение.

Было и противоположное: в одном проекте декораторы усложнили отладку, потому что стек вызовов стал менее очевидным. Мы откатились на более явные реализации, оставив декораторы только для тестовых и вспомогательных сценариев.

Короткая памятка: что проверить перед внедрением

Перед тем как использовать декораторы в проекте, пройдитесь по нескольким пунктам. Это поможет избежать нежелательных сюрпризов в будущем.

  • Нужны ли вам метаданные или достаточно простых фабрик и функций?
  • Как будет решаться вопрос тестирования и отладки декораторов?
  • Как изменения в tsconfig повлияют на сборку и на совместимость с другими инструментами?

Декораторы с метаданными дают мощный, но не всесильный инструмент. Они экономят время в шаблонных задачах и улучшают читаемость, если используются аккуратно и документированно. В критичных по производительности или по безопасности местах стоит предпочесть явность и простые подходы.