Декораторы и метаданные привносят в 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 повлияют на сборку и на совместимость с другими инструментами?
Декораторы с метаданными дают мощный, но не всесильный инструмент. Они экономят время в шаблонных задачах и улучшают читаемость, если используются аккуратно и документированно. В критичных по производительности или по безопасности местах стоит предпочесть явность и простые подходы.

