Перейти к основному содержимому

Декораторы TypeScript

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

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


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

Современный TypeScript (5.0 и новее) поддерживает стандартный (TC39) синтаксис декораторов сразу — никакая опция компилятора не нужна, и именно эта форма используется во всех примерах на этой странице.

Старый код использует устаревшую (legacy) схему декораторов. Чтобы скомпилировать такую форму, её нужно включить в tsconfig.json:

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

Примечание: emitDecoratorMetadata заставляет компилятор выдавать информацию о типах для устаревших декораторов. Библиотеки, такие как TypeORM и class-validator, опираются на неё.


Виды декораторов

TypeScript позволяет декорировать объявления нескольких видов. Со стандартными декораторами каждый декоратор получает два аргумента: значение, которое декорируется, и объект контекста, который сообщает его вид и имя.

  • Декоратор класса — применяется к объявлениям классов: (constructor: Function, context: ClassDecoratorContext) => void | Function
  • Декоратор метода — применяется к методам класса: (method: Function, context: ClassMethodDecoratorContext) => void | Function
  • Декоратор аксессора — применяется к полям accessor (стандартный способ перехватывать чтение и запись свойства): (target: { get, set }, context: ClassAccessorDecoratorContext) => { get?, set?, init? }
  • Декоратор поля — применяется к обычным свойствам класса: (value: undefined, context: ClassFieldDecoratorContext) => void | ((initialValue) => any)

Устаревшая схема вместо этого передаёт (target, propertyKey, descriptor) декораторам методов, (target, propertyKey) — декораторам свойств и (target, propertyKey, parameterIndex) — декораторам параметров. Декораторы параметров существуют только в устаревшей схеме.


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

Декоратор класса получает конструктор класса. С его помощью можно наблюдать за классом, изменить его или даже вернуть новый конструктор, который его заменит.

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

Простой декоратор класса

Этот простой декоратор выводит сообщение в тот момент, когда класс определяется.

Prompt
Покажите простой декоратор класса, который выполняется при объявлении класса. Напишите декоратор logClass, который принимает конструктор класса и ClassDecoratorContext и выводит Класс <name> определён в <метка времени ISO>. Примените его с помощью @logClass к классу UserService с методом getUsers(), возвращающим ["Alice", "Bob", "Charlie"]. Больше ничего не вызывается — строка журнала появляется, как только файл загружается.
Вывод: программа на TypeScript и захваченный вывод

Декоратор класса с изменением конструктора

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

Prompt
Покажите фабрику декораторов класса, которая изменяет класс и заменяет его конструктор. Напишите versioned(version: string), возвращающую декоратор класса, который задаёт prototype.version, а затем возвращает конструктор-обёртку, выводящий Создание экземпляра <name> v<version> и вызывающий оригинал через new, копируя прототип, чтобы instanceof продолжал работать. Примените @versioned("1.0.0") к классу ApiClient с методом fetchData(), который выводит Получение данных...; создайте экземпляр, выведите его version (приведя к any) и вызовите fetchData().
Вывод: программа на TypeScript и захваченный вывод

Декоратор запечатывания класса

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

Prompt
Покажите декоратор класса, который запечатывает класс, чтобы в него нельзя было добавить новые члены. Напишите декоратор sealed, который выводит Запечатывание <name>... и вызывает Object.seal и для конструктора, и для его прототипа. Примените его с помощью @sealed к классу Greeter, который хранит greeting: string, задаваемое в конструкторе, и имеет метод greet(), возвращающий Привет, <greeting>. Оставьте закомментированную строку, показывающую, что добавление Greeter.prototype.newMethod завершилось бы ошибкой.
Вывод: программа на TypeScript и захваченный вывод

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

Декоратор метода ставится на метод и может наблюдать за ним, изменить или заменить его.

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

  1. originalMethod: сама функция метода
  2. context: объект ClassMethodDecoratorContext с именем метода name, признаками static и private, а также хуком addInitializer

Если декоратор возвращает функцию, эта функция заменяет метод. Такая обёртка вокруг оригинала — это и есть способ добавить логирование, измерение времени или проверки вокруг вызова.

Главное о декораторах методов:

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

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

Этот декоратор измеряет и выводит, сколько времени занимает выполнение метода.

Prompt
Покажите декоратор метода, который измеряет, сколько времени занимает выполнение метода. Напишите measureTime(originalMethod, context: ClassMethodDecoratorContext), возвращающую обёртку, которая запоминает performance.now() до и после вызова оригинала и выводит <method> выполнен за <ms> мс с двумя знаками после запятой. Примените его к processData(data: number[]): number[] в классе DataProcessor, который выполняет 100 000 000 итераций цикла, имитируя работу, и возвращает каждое значение удвоенным; вызовите его с [1, 2, 3, 4, 5].
Вывод: программа на TypeScript и захваченный вывод

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

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

Prompt
Покажите управление доступом на основе ролей, реализованное с помощью фабрики декораторов метода. Определите type UserRole = "admin" | "editor" | "viewer" и объект currentUser (id 1, имя John Doe, роли ["viewer"]). Напишите AllowedRoles(...roles), возвращающую декоратор метода, обёртка которого выбрасывает Пользователь <name> не имеет права вызывать <method>, когда у текущего пользователя нет ни одной из этих ролей. В классе DocumentService разрешите deleteDocument для admin/editor, а viewDocument — для всех трёх ролей; внутри try/catch вызовите сначала просмотр, затем удаление от имени viewer (второй вызов выбрасывает ошибку, и её сообщение печатается через console.error), затем смените роли на ["admin"] и удалите снова, уже успешно.
Вывод: программа на TypeScript и захваченный вывод

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

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

Prompt
Покажите фабрику декораторов метода, которая печатает предупреждение об устаревании перед выполнением метода. Напишите deprecated(message: string), возвращающую декоратор метода, обёртка которого вызывает console.warn с текстом Предупреждение: <method> устарел. <message>, а затем исходный метод. В классе PaymentService пометьте processPayment(amount, currency) с помощью @deprecated("Используйте processPaymentV2"), оставьте processPaymentV2 без декоратора и вызовите оба с аргументами 100, "USD".
Вывод: программа на TypeScript и захваченный вывод

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

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

Со стандартными декораторами свойство объявляется с ключевым словом accessor. Тогда декоратор получает два параметра:

  1. target: объект со сгенерированными функциями get и set
  2. context: объект ClassAccessorDecoratorContext с именем свойства name

Если вернуть объект с собственными get и set, они заменят аксессоры по умолчанию.

Главное о декораторах свойств:

  • Они выполняются, когда класс определяется, а не когда обращаются к свойству
  • Декоратор на обычном поле (без accessor) может только скорректировать начальное значение; чтобы перехватывать чтение и запись, используйте accessor
  • В устаревшей схеме они получают (target, propertyKey) и должны использовать Object.defineProperty, чтобы изменить поведение
  • Их часто применяют для форматирования, логирования, проверки данных или метаданных
  • Их можно сочетать с другими декораторами для более сложного поведения

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

Этот декоратор автоматически форматирует свойство каждый раз, когда ему присваивают значение.

Prompt
Покажите фабрику декораторов свойства (авто-аксессора), которая форматирует значение при каждой записи. Напишите format(formatString: string), возвращающую декоратор аксессора, который хранит приватное value и возвращает пару get/set, где сеттер сохраняет formatString с заменой {} на новое значение. Примените @format("Привет, {}!") к accessor greeting: string в классе Greeter, присвойте greeting значение "Мир" и выведите его (ожидается Привет, Мир!).
Вывод: программа на TypeScript и захваченный вывод

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

Этот декоратор записывает в журнал каждое чтение и каждое изменение свойства.

Prompt
Покажите декоратор аксессора, который записывает в журнал каждое чтение и каждую запись свойства. Напишите logProperty, который возвращает геттер, выводящий Чтение <name>: <value>, и сеттер, выводящий Запись <name> с <old> на <new>. Примените его к accessor name: string и accessor price: number класса Product, конструктор которого присваивает оба. Создайте new Product("Ноутбук", 999.99), присвойте price значение 899.99, затем выведите product.name.
Вывод: программа на TypeScript и захваченный вывод

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

Этот декоратор следит за тем, чтобы свойству было присвоено значение до того, как его прочитают.

Prompt
Покажите декоратор аксессора, который требует, чтобы у свойства было значение до того, как его прочитают. Напишите required, геттер которого выбрасывает Свойство <name> обязательно, когда сохранённое значение равно undefined, а сеттер просто сохраняет значение. Примените его к accessor username: string и accessor email: string класса User (с необязательным age?: number), конструктор которого присваивает оба. Создайте new User("johndoe", "john@example.com") — это работает — и оставьте закомментированные строки, показывающие, что передача undefined привела бы к ошибке. Программа ничего не печатает.
Вывод: программа на TypeScript и захваченный вывод

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

В устаревшей схеме декораторов можно также декорировать отдельный параметр конструктора или метода. Такой декоратор получает три параметра:

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

В стандартных декораторах декораторов параметров нет. Пример ниже сохраняет идею — описать правило для каждого параметра, а затем проверять аргументы при вызове метода, — но передаёт правила декоратору метода в порядке параметров, поэтому он компилируется с настройками по умолчанию.

Главное о декораторах параметров:

  • Они выполняются, когда метод определяется, а не когда он вызывается
  • Обычно их сочетают с декоратором метода, который делает основную работу
  • С библиотекой reflect-metadata они могут сохранять и читать метаданные
  • Они распространены во фреймворках внедрения зависимостей
  • Индекс параметра позволяет найти соответствующий аргумент во время выполнения

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

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

Prompt
Покажите проверку параметров с помощью декораторов, написанную на стандартных (TC39) декораторах, у которых нет декораторов параметров. Напишите validateParam(type), которая возвращает объект правила для "string" | "number" | "boolean", и декоратор метода validate(...rules), который преобразует правила в записи { index, type } и при каждом вызове проверяет каждый аргумент (непустая строка, число, не являющееся NaN, логическое значение) и при несоответствии выбрасывает Параметр с индексом <i> не прошёл проверку типа <type>. Примените @validate(validateParam("string"), validateParam("number"), validateParam("boolean")) к createUser(name, age, isActive) в классе UserService, который выводит Создание пользователя: <name>, <age>, <isActive>; вызовите его с "John", 30, true и оставьте закомментированные вызовы, которые завершились бы ошибкой.
Вывод: программа на TypeScript и захваченный вывод

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

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

Поскольку фабрику вы вызываете с аргументами, декоратор, который она создаёт, можно настроить. Это делает декораторы гибче и пригоднее для повторного использования.

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

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

Prompt
Покажите фабрику декораторов, которая принимает объект конфигурации. Напишите logWithConfig(config: { level: "log" | "warn" | "error", message?: string }), возвращающую декоратор метода, обёртка которого печатает <message>: <method> вместе с аргументами через console[level], выполняет исходный метод, а затем печатает <method> завершён. Примените его с уровнем log и сообщением Обработка платежа к processPayment(amount: number) в классе PaymentService, который выводит Обработка платежа на $<amount>. Только объявите класс — ничего не вызывается, поэтому вывода нет.
Вывод: программа на TypeScript и захваченный вывод

Порядок вычисления

Когда на одном объявлении стоит несколько декораторов, фабрики вычисляются сверху вниз, а затем возвращённые ими декораторы вызываются снизу вверх.

Prompt
Покажите порядок, в котором вычисляются фабрики декораторов и возвращаемые ими декораторы, когда на одном методе стоит сразу несколько. Напишите две фабрики first() и second(), каждая из которых выводит <name>(): фабрика вычислена и возвращает декоратор метода, выводящий <name>(): вызван. Поставьте @first() над @second() на пустом методе method() в классе ExampleClass. Ожидаемый порядок: обе фабрики сверху вниз, затем декораторы снизу вверх (second(): вызван перед first(): вызван).
Вывод: программа на TypeScript и захваченный вывод

Примеры из реальной практики

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

В этом примере декораторы описывают небольшой API-контроллер, похоже на то, как это делают NestJS или фреймворки на основе Express.

Prompt
Покажите, как декораторы могут регистрировать маршруты небольшого API-контроллера, в стиле NestJS или Express. Заведите массив ROUTES. Напишите декоратор класса Controller(prefix), который сохраняет префикс в прототипе и назначает класс собранным до этого момента маршрутам, и декоратор метода Get(path), который добавляет { method: "get", path, handler }. Декорируйте UserController с помощью @Controller("/users") и два метода @Get("/") getAllUsers() и @Get("/:id") getUserById(id). Функция registerRoutes() выводит Зарегистрирован GET <prefix><path> для каждого маршрута.
Вывод: программа на TypeScript и захваченный вывод

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

Придерживайтесь этих правил, когда пишете декораторы:

  1. Делайте декораторы узкими: каждый декоратор должен делать одну вещь.
  2. Описывайте поведение: ясно говорите, что делает декоратор и какие у него побочные эффекты.
  3. Используйте фабрики декораторов: делайте декораторы настраиваемыми, чтобы их можно было применять повторно.
  4. Учитывайте производительность: декоратор, который оборачивает метод, добавляет работу к каждому вызову.
  5. Безопасность типов: используйте типы контекста (ClassMethodDecoratorContext и подобные), чтобы декораторы оставались типобезопасными.
  6. Обработка ошибок: корректно обрабатывайте ошибки внутри декораторов.
  7. Тестирование: пишите модульные тесты для своих декораторов.
  8. Метаданные: используйте reflect-metadata для сложных случаев, где нужна информация о типах во время выполнения.

Частые ошибки

Избегайте этих распространённых ошибок:

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

Два фрагмента ниже используют устаревший синтаксис. Чтобы их скомпилировать, добавьте в tsconfig.json: "compilerOptions": { "experimentalDecorators": true }.

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

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

Устаревший декоратор свойства readonly

Декоратор свойства в устаревшем стиле, который с помощью Object.defineProperty делает name недоступным для записи.

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

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

Устаревший декоратор параметра

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

script.ts
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter in ${propertyKey} at index ${parameterIndex}`);
}

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