Декоратор это функция, которую вы прикрепляете к классу или члену класса через @name. Она получает исходный метод (или класс, или поле) и объект контекста, который его описывает, и может вернуть замену. TypeScript поддерживает стандартные декораторы без флагов компилятора.
@logged выполняется один раз, при определении класса, и заменяет add обёрткой, которую возвращает. Каждый вызов проходит через обёртку. Обобщённые параметры сохраняют типы this, аргументов и возвращаемого значения метода, так что add по-прежнему принимает два числа и возвращает число.
Как компилируются декораторы
Стандартные декораторы происходят из предложения TC39 для JavaScript, и TypeScript реализует их с версии TypeScript 5.0. Предложение ещё не входит в стандарт JavaScript, а Node 24 не разбирает синтаксис @, поэтому при цели ES2022 (как на этих страницах) компилятор переписывает каждый декорированный класс в обычный JavaScript, который вызывает вспомогательные функции (__esDecorate и __runInitializers, выводятся в начале файла). Результат работает везде, где работает ES2022.
Одно следствие: файл с декораторами нельзя запустить через встроенное удаление типов в Node (node file.ts), которое только убирает типы и оставляет @ на месте. Node останавливается с SyntaxError: Invalid or unexpected token. Сначала скомпилируйте файл через tsc или бандлер.
Виды декораторов и их сигнатуры
У каждого стандартного декоратора форма (value, context) => replacement | void. Что такое value и что можно вернуть, зависит от того, что декорируется:
| Декорирует | value | Тип контекста | Возвращает |
|---|---|---|---|
| класс | класс | ClassDecoratorContext | класс на замену или ничего |
| метод | метод | ClassMethodDecoratorContext | метод на замену |
| геттер / сеттер | геттер или сеттер | ClassGetterDecoratorContext / ClassSetterDecoratorContext | геттер или сеттер на замену |
| поле | undefined | ClassFieldDecoratorContext | функцию, преобразующую начальное значение |
поле accessor | { get, set } | ClassAccessorDecoratorContext | { get?, set?, init? } |
У каждого объекта контекста есть kind, name и addInitializer. У контекста члена класса также есть static, private и объект access для чтения члена из экземпляра. Декораторы работают и со статическими членами, и с членами #private.
Фабрики декораторов
Чтобы передать параметры, напишите функцию, которая возвращает декоратор, и вызовите её в месте @. Это фабрика декораторов:
@retry(3) сначала вызывает retry, а функция, которую она возвращает, и есть настоящий декоратор. Несколько декораторов складываются: в @a @b method() сначала применяется b, а a оборачивает результат.
Декораторы класса и addInitializer
Декоратор класса получает сам класс. Он может вернуть подкласс на замену или ничего не возвращать, а просто где-то его зарегистрировать. context.addInitializer регистрирует код, который выполнится в определённый момент: для декоратора класса сразу после полного определения класса, для декоратора метода при создании каждого экземпляра.
Декоратор класса ставится над классом (или после export). Без @bound вызов loose() выбросил бы ошибку, потому что this был бы undefined.
Декораторы полей и accessor
Декоратор поля не видит и не может перехватить последующие присваивания: его value равно undefined, и вернуть он может только функцию, которая преобразует начальное значение поля. Чтобы перехватывать чтение и запись, объявите поле с ключевым словом accessor, которое превращает его в пару геттера и сеттера с приватным хранилищем, и декорируйте его:
accessor входит в то же предложение. Он генерирует настоящие геттер и сеттер поверх поля #private, поэтому p.price = -5 проходит через set декоратора.
Стандартные декораторы и устаревшие experimentalDecorators
До TypeScript 5.0 единственными декораторами в TypeScript была ранняя версия предложения, включаемая через experimentalDecorators. Этот флаг всё ещё существует и переключает компилятор на устаревшую модель с другими сигнатурами и семантикой:
| Стандартные (без флага) | Устаревшие (experimentalDecorators) | |
|---|---|---|
| Сигнатура | (value, context) | (target, propertyKey, descriptor) |
| Как меняет метод | возвращает новую функцию | изменяет descriptor.value |
| Декораторы параметров | не поддерживаются (TS1206) | поддерживаются |
emitDecoratorMetadata | не поддерживается | поддерживается (информация о типах во время выполнения через reflect-metadata) |
Декораторы accessor ({ get, set, init }), addInitializer | да | нет |
| Основа | предложение TC39 | его ранний черновик |
Декоратор, написанный для одной модели, не проходит проверку типов в другой. Вот декоратор в устаревшем стиле в проекте без флага:
index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
Исправить можно двумя способами: переписать его в стандартной форме (value, context), как в первом примере на этой странице, или включить устаревшую модель для всего проекта:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Angular, NestJS и TypeORM по-прежнему задают experimentalDecorators в конфигурациях, которые генерируют их инструменты, и требуют его в документации. NestJS и TypeORM нужен ещё и emitDecoratorMetadata, потому что они читают типы во время выполнения: NestJS для внедрения параметров конструктора, TypeORM для сопоставления свойств с колонками:
// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
constructor(@Inject(DB) private db: Database) {}
}
Если вы используете один из этих фреймворков, пишите декораторы по-старому и следуйте его документации. Для нового кода без такого фреймворка используйте стандартные декораторы.
Когда использовать декораторы
Декораторы подходят для сквозного поведения, которое иначе повторялось бы во многих методах: логирование, замер времени, кеширование, повторные попытки, проверка доступа, валидация и регистрация классов в контейнере или маршрутизаторе. При этом они скрывают поток управления: читателю нужно посмотреть, что делает @retry, прежде чем понять, что делает метод. Для одного-двух применений обычная функция высшего порядка (const fetchData = retry(3, rawFetch)) проще и работает и вне классов.
Часто задаваемые вопросы
Что такое декораторы в TypeScript?
Декоратор это функция, применяемая к классу или члену класса через синтаксис @name. Она получает декорируемую сущность и объект контекста и может вернуть замену: обёрнутый метод, новый класс или функцию, преобразующую начальное значение поля. Обычно их используют для логирования, валидации, кеширования и регистрации классов.
Нужен ли experimentalDecorators, чтобы использовать декораторы в TypeScript?
Нет. Начиная с TypeScript 5.0 стандартные декораторы (TC39) работают без флага. experimentalDecorators переключает компилятор на старую, устаревшую модель декораторов, на которой построены фреймворки вроде Angular и NestJS. У этих двух моделей разные сигнатуры функций, и они не взаимозаменяемы.
Чем стандартные декораторы отличаются от experimentalDecorators?
Стандартные декораторы получают (value, context) и возвращают замену. Устаревшие получают (target, propertyKey, descriptor) и изменяют дескриптор свойства. Только устаревшая модель поддерживает декораторы параметров и emitDecoratorMetadata; только в стандартной есть декораторы accessor, возвращающие { get, set, init }, и context.addInitializer.
Поддерживает ли TypeScript декораторы параметров?
Только с включённым experimentalDecorators. В стандартном режиме декоратор на параметре это ошибка TS1206: Decorators are not valid here, потому что предложение TC39 не включает декораторы параметров. Поэтому фреймворкам внедрения зависимостей, которые декорируют параметры конструктора, нужен устаревший флаг.
В каком порядке применяются несколько декораторов?
Выражения декораторов вычисляются сверху вниз, а применяются снизу вверх: в @a @b method() сначала b оборачивает метод, а затем a оборачивает результат. Поэтому при вызове метода a выполняется снаружи.