Декоратори – це можливість TypeScript, яка дозволяє застосовувати принципи метапрограмування. Простими словами, декоратор – це спеціальна обгортка, що дає змогу додати або змінити поведінку частин коду без зміни їхнього вихідного коду. При цьому додаткова логіка підключається на етапі декларації, ще до того, як код почне виконуватися.
Завдяки цьому декоратори часто використовуються для опису конфігурації та службової логіки, а не для бізнес-логіки безпосередньо.
Декоратори можна часто зустріти у популярних фреймворках, таких як NestJS або Angular. Там вони виглядають як анотації, що починаються з символу @: @Component, @Injectable, @Get. Саме через декоратори у таких фреймворках описуються компоненти, маршрути, залежності та інші інфраструктурні аспекти застосунку.
Зміст допису:
- Про стандарт декораторів у цьому дописі
- Де можна застосовувати декоратори
- Декоратор класу
- Декоратор методу
- Декоратор властивості
- Декоратор параметра
- Декоратор accessor (get/set)
- Комбінування декораторів (Decorator Composition)
- Практичне застосування декораторів у TypeScript
- Обмеження, які варто знати
Про стандарт декораторів у цьому дописі
У TypeScript на сьогодні існують два підходи до використання декораторів:
- Новий стандарт (TC39 / Stage 3) – сучасний підхід до декораторів, запропонований для стандарту JavaScript та реалізований у TypeScript, починаючи з версії 5.0.
- Експериментальні (legacy) декоратори – старіший механізм, який вмикається опцією experimentalDecorators у файлі tsconfig.json. Він досі використовується у багатьох існуючих фреймворках.
У цьому дописі наведені приклади для нового стандарту декораторів (Stage 3). Він не потребує додаткових налаштувань компілятора та працює «з коробки».
Якщо ваш проєкт використовує legacy-декоратори (наприклад, у NestJS або TypeORM), синтаксис, сигнатури та поведінка декораторів можуть суттєво відрізнятися від наведених прикладів.
Де можна застосовувати декоратори
TypeScript дозволяє застосовувати декоратори до наступних типів декларацій:
- Class decorator – застосовується до класу та дозволяє змінювати або розширювати його поведінку.
- Method decorator – застосовується до методу класу.
- Property decorator – застосовується до властивості класу.
- Parameter decorator – застосовується до параметра методу або конструктора.
- Accessor decorator – застосовується до гетерів та сеттерів get/set.
(Примітка: Декоратори параметрів, які ви могли бачити у старих проєктах, наразі не є частиною стандарту Stage 3).
Декоратор класу
Декоратор класу – це функція, яка виконується на етапі визначення класу та дозволяє втрутитися в процес його створення.
У новому стандарті декораторів TypeScript (Stage 3) декоратор класу отримує два аргументи:
- value – сам клас (його конструктор)
- context – об’єкт з метаданими класу (наприклад, name, addInitializer)
Завдяки цьому декоратор може:
- читати метадані класу
- змінювати або доповнювати поведінку конструктора
- повертати новий клас (повна заміна)
- виконувати побічні дії (логування, реєстрація тощо)
Приклад логування створення класу:
function Log(value: Function, context: ClassDecoratorContext): void {
console.log('Class initialized:', context.name);
}
@Log
class User {
name = 'Alex';
greet() {
console.log(`Hello, I am ${this.name}`);
}
}
const user = new User();
user.greet();
/**
* Console output:
*
* Class initialized: User
* Hello, I am Alex
*/Пояснення:
- Декоратор @Log виконується під час визначення класу, у момент, коли клас створюється інтерпретатором JavaScript, ще до виконання будь-якого коду, що створює його екземпляри.
- Через context.name декоратор отримує ім’я класу.
- У цьому прикладі декоратор виконує лише побічну дію – логування; поведінка класу та його екземплярів не змінюється.
- Декоратор класу виконується один раз, незалежно від кількості створених екземплярів.
Декоратори класів зазвичай використовуються для реєстрації класів, ініціалізації конфігурацій, інтеграції з фреймворками та логування.
Декоратор методу
Декоратор методу дозволяє перехопити та змінити поведінку методу класу. У новому синтаксисі декораторів (TC39 / Stage 3) він отримує два аргументи:
- value – функція, що реалізує початкову логіку методу до застосування декоратора
- context – об’єкт з метаданими про декорований метод (наприклад: name, kind, static, private, addInitializer)
Декоратор може повернути нову функцію, яка повністю замінює оригінальний метод. Якщо декоратор нічого не повертає, поведінка методу залишається незмінною.
Приклад логування викликів методу:
function LogMethod(value: Function, context: ClassMethodDecoratorContext) {
return function (this: unknown, ...args: unknown[]) {
console.log(`Call: ${String(context.name)}`, args);
return value.apply(this, args);
};
}
class Calculator {
@LogMethod
sum(a: number, b: number): number {
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.sum(2, 3);
console.log(`Result: ${result}`);
/**
* Console output:
*
* Call: sum [ 2, 3 ]
* Result: 5
*/Як це працює:
- Декоратор отримує оригінальний метод sum як value та інформацію про нього в context.
- Декоратор повертає функцію-обгортку, яка замінює оригінальний метод.
- Під час виклику методу спочатку виконується логування аргументів.
- Виклик value.apply(this, args) забезпечує виконання оригінального методу з коректним значенням this, яке відповідає поточному екземпляру класу.
Декоратор властивості
Декоратор властивості виконується на етапі визначення класу та дозволяє втрутитися в процес опису поля класу.
У новому стандарті декораторів (TC39 / Stage 3) декоратор властивості отримує два аргументи:
- value – завжди undefined, оскільки поля класу ініціалізуються лише після створення екземпляра
- context – об’єкт з метаданими про декоровану властивість (наприклад: name, static, private, addInitializer)
Декоратор властивості не може перехоплювати читання або зміну значення поля під час роботи програми (для цього існують аксесори). Його основне призначення – збір метаданих або зміна початкової ініціалізації.
Приклад позначення обов’язкової властивості:
function Required(_value: unknown, context: ClassFieldDecoratorContext): void {
console.log(`Property marked as required: ${String(context.name)}`);
}
class Form {
@Required
email!: string;
}
const form = new Form();
form.email = 'test@example.com';
console.log('Email value:', form.email);
/**
* Console output:
*
* Property marked as required: email
* Email value: test@example.com
*/Як це працює:
- Декоратор @Required виконується під час оголошення класу Form, а не під час створення його екземплярів.
- Через context.name декоратор отримує ім’я властивості (email).
- На цьому етапі значення властивості ще не існує, тому декоратор не може його прочитати або змінити.
- Декоратори властивостей зазвичай використовуються для:
- збору метаданих (Dependency Injection)
- валідації полів
- налаштування ORM-мапінгу
Декоратор параметра
У новому стандарті декораторів TypeScript (TC39 / Stage 3) декоратори параметрів не підтримуються. Синтаксис виду @Decorator param: type, який використовувався у legacy-декораторах, більше не застосовується.
Натомість, якщо необхідно обробити або проаналізувати значення параметрів методу, це робиться через декоратор методу, який має доступ до всіх аргументів під час виклику.
Нижче наведено приклад, що відтворює ідею декоратора параметра – логування конкретного аргументу методу:
function LogParamAt(index: number) {
return function (value: Function, context: ClassMethodDecoratorContext) {
return function (this: unknown, ...args: unknown[]) {
console.log(`Parameter at position ${index} in method ${String(context.name)}:`, args[index]);
return value.apply(this, args);
};
};
}
class Service {
@LogParamAt(0)
run(id: number) {
console.log('Run with id:', id);
}
}
const service = new Service();
service.run(42);
/**
* Console output:
*
* Parameter at position 0 in method run: 42
* Run with id: 42
*/Як це працює:
- LogParamAt – створює декоратор методу з фіксованим індексом параметра.
- Декоратор обгортає метод run і отримує всі передані йому аргументи у вигляді масиву args.
- Значення потрібного параметра визначається за індексом args[index].
- Оригінальний метод викликається через apply, що забезпечує коректне значення this.
Такий підхід зазвичай використовується для:
- логування параметрів
- валідації аргументів
- авторизації
- трасування викликів
- реалізації behavior-based логіки
Декоратор accessor (get/set)
Для роботи з новим ключовим словом accessor використовується спеціальний тип декоратора. Ключове слово accessor (введене в TS 5.0) автоматично створює приватне поле класу та методи get/set для доступу до нього.
У новому стандарті декораторів (TC39 / Stage 3) декоратор аксесора отримує два аргументи:
- value – об’єкт, що містить методи доступу get та set
- context – об’єкт з метаданими аксесора (наприклад: name, static, private)
Декоратор може повернути новий об’єкт із методами get, set та init. Метод init дозволяє змінити початкове значення поля при створенні екземпляра класу.
Приклад блокування запису (Read-only accessor):
function BlockSet<This, Value>(
value: ClassAccessorDecoratorTarget<This, Value>,
context: ClassAccessorDecoratorContext<This, Value>,
): ClassAccessorDecoratorResult<This, Value> {
return {
get(this: This): Value {
return value.get.call(this);
},
set(this: This, _value: Value): void {
throw new Error(`Setting ${String(context.name)} is not allowed`);
},
init(initialValue: Value): Value {
return initialValue;
},
};
}
class Config {
@BlockSet
accessor token = 'default_token';
}
const c = new Config();
console.log('Initial token:', c.token);
try {
c.token = 'new_secret';
} catch (e) {
console.log('Error:', (e as Error).message);
}
/**
* Console output:
*
* Initial token: default_token
* Error: Setting token is not allowed
*/Як це працює:
- Декоратор @BlockSet перехоплює створення аксесора token.
- Він повертає нову реалізацію методів доступу:
- get працює як раніше (читає значення)
- set повністю блокує зміну значення, викидаючи помилку
- init просто пропускає початкове значення ('default_token') далі
- Завдяки ключовому слову accessor, нам не потрібно створювати окреме приватне поле класу для зберігання значення – TypeScript робить це автоматично.
Комбінування декораторів (Decorator Composition)
До однієї декларації класу можна застосувати кілька декораторів. У такому випадку вони застосовуються знизу вгору – від декоратора, який записаний найближче до декларації, до найвіддаленішого.
Інакше кажучи, декоратор, розташований нижче у коді, виконується першим. Це правило однаково працює для декораторів класів, методів, accessor-декораторів та властивостей.
Приклад:
function A(value: Function, context: ClassDecoratorContext) {
console.log('A');
}
function B(value: Function, context: ClassDecoratorContext) {
console.log('B');
}
@A
@B
class Example {}
/**
* Console output:
*
* B
* A
*/Запис:
@A
@B
class Example {}еквівалентний наступній композиції викликів:
Example = A(B(Example));Саме тому спочатку виконується B, а вже потім A.
Практичне застосування декораторів у TypeScript
Декоратори використовуються для розширення або модифікації поведінки класів і методів без зміни їх основного коду. Розглянемо поширені практичні сценарії використання.
1. Логування (класи, методи)
Декоратори часто застосовуються для автоматичного логування:
- викликів методів
- переданих аргументів
- створення екземплярів класів
- тривалості виконання операцій
Такий підхід дозволяє додати логування без втручання в бізнес-логіку.
2. Кешування (методи)
Декоратор може зберігати результат виконання методу та повертати його при повторних викликах з тими самими аргументами. Це особливо корисно для важких обчислень або повторюваних запитів до API.
3. Контроль доступу
Через декоратори зручно реалізовувати перевірки доступу, зокрема:
- перевірку авторизації користувача
- перевірку наявності необхідних прав (ролей)
- валідацію доступу до конкретного методу
Логіка доступу при цьому залишається відокремленою від основного коду.
4. Валідація властивостей
Декоратори властивостей використовуються для:
- позначення обов’язкових полів
- опису обмежень (наприклад, довжина рядка, формат email)
- прив’язки правил валідації
Такий підхід широко застосовується у формах, DTO та ORM-моделях.
5. Dependency Injection у Angular
У Angular декоратори є ключовим механізмом конфігурації:
- @Injectable()
- @Component()
- @Input()
- @Output()
Вони дозволяють фреймворку збирати метадані та автоматично створювати й зв’язувати залежності.
6. Реєстрація класів у NestJS
NestJS активно використовує декоратори для опису структури застосунку:
- @Controller()
- @Get(), @Post()
- @Module()
- @Inject()
На основі цих метаданих фреймворк будує маршрути, контейнер залежностей і внутрішню архітектуру застосунку.
Обмеження, які варто знати
Відсутність нативної підтримки у старих середовищах
Хоча декоратори вже увійшли до стандарту ECMAScript, багато браузерів та старі версії Node.js не підтримують їх нативно.Тому код з декораторами зазвичай потребує транспіляції за допомогою TypeScript, Babel або збірників на кшталт Vite.
Декоратори – це код runtime
Декоратори є звичайними функціями, які виконуються під час роботи програми.TypeScript лише перетворює синтаксис @Decorator на відповідні виклики функцій, а вся логіка декоратора працює вже у JavaScript runtime.
Виконуються лише один раз
Декоратори застосовуються під час визначення класу, тобто в момент завантаження модуля, а не при створенні нових екземплярів через new User().
Якщо потрібна логіка, яка має виконуватися при кожному виклику методу, декоратор повинен повертати нову функцію-обгортку, що замінює оригінальний метод.
Обмеження декораторів властивостей у legacy-режимі
У режимі experimentalDecorators (legacy) декоратори властивостей не мають доступу до фактичного значення поля. Вони отримують лише посилання на клас та ім’я властивості, тому їх основне призначення – збір і збереження метаданих.
Саме така поведінка використовується у більшості популярних фреймворків, зокрема NestJS та Angular, які досі працюють у legacy-семантиці декораторів.
Новий стандарт (Stage 3) пропонує іншу, більш безпечну модель роботи з полями, проте старий підхід досі залишається домінуючим в екосистемі через інерцію великих фреймворків.
Підсумок
Декоратори у TypeScript дозволяють розширювати поведінку класів і їх декларацій без зміни основної логіки коду. Вони надають зручний механізм роботи з метаданими та інкапсуляції службової логіки, що активно використовується у сучасних фреймворках.
Новий стандарт декораторів (TC39 / Stage 3) робить цей механізм більш передбачуваним і наближеним до стандарту JavaScript, хоча на практиці багато екосистем досі використовують legacy-підхід.
Документація – TypeScript 5.0 Release Notes: Decorators