Декораторы — мощная возможность TypeScript, позволяющая добавлять метаданные и модифицировать классы и их члены на этапе проектирования.
Они широко применяются во фреймворках вроде Angular и NestJS для внедрения зависимостей, маршрутизации и других задач.
Включение декораторов
Чтобы использовать декораторы в TypeScript, их нужно активировать в файле tsconfig.json:
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
},
"include": ["src/**/*.ts"]
}
Примечание: Опция emitDecoratorMetadata включает экспериментальную поддержку генерации метаданных типов для декораторов — это используется в библиотеках вроде TypeORM и валидаторах классов.
Типы декораторов
TypeScript поддерживает несколько типов декораторов, которые можно применять к разным объявлениям:
| Тип декоратора | Применяется к | Сигнатура |
|---|---|---|
| Декоратор класса | Объявлениям классов | (constructor: Function) => void |
| Декоратор метода | Методам класса | (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void |
| Декоратор свойства | Свойствам класса | (target: any, propertyKey: string) => void |
| Декоратор параметра | Параметрам методов | (target: any, propertyKey: string, parameterIndex: number) => void |
Декораторы классов
Декораторы классов применяются к конструктору класса и позволяют наблюдать за определением класса, модифицировать его или заменять.
Они вызываются при объявлении класса — не при создании экземпляров.
Базовый декоратор класса
Этот простой декоратор логирует момент определения класса:
// Простой декоратор класса, логирующий его определение
function logClass(constructor: Function) {
console.log(`Класс ${constructor.name} определён в ${new Date().toISOString()}`);
}
// Применение декоратора
@logClass
class UserService {
getUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}
// Вывод при загрузке файла: "Класс UserService определён в [timestamp]"
Декоратор класса с модификацией конструктора
Пример, показывающий, как модифицировать класс, добавляя свойства и методы:
// Декоратор, добавляющий свойство версии и логирующий создание экземпляра
function versioned(version: string) {
return function (constructor: Function) {
// Добавляем статическое свойство
constructor.prototype.version = version;
// Сохраняем оригинальный конструктор
const original = constructor;
// Создаём новый конструктор, оборачивающий оригинальный
const newConstructor: any = function (...args: any[]) {
console.log(`Создаётся экземпляр ${original.name} v${version}`);
return new original(...args);
};
// Копируем прототип, чтобы работал instanceof
newConstructor.prototype = original.prototype;
return newConstructor;
};
}
// Применяем декоратор с версией
@versioned('1.0.0')
class ApiClient {
fetchData() {
console.log('Загружаем данные...');
}
}
const client = new ApiClient();
console.log((client as any).version); // Выводит: 1.0.0
client.fetchData();
Декоратор "запечатанного" класса
Этот декоратор запрещает добавление новых свойств в класс и помечает все существующие свойства как неконфигурируемые:
function sealed(constructor: Function) {
console.log(`Запечатываем ${constructor.name}...`);
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return `Привет, ${this.greeting}`;
}
}
// В строгом режиме это вызовет ошибку
// Greeter.prototype.newMethod = function() {}; // Ошибка: нельзя добавить свойство newMethod
Ключевые моменты о декораторах классов
- Декораторы классов вызываются при объявлении класса, а не при создании экземпляров.
- Они получают конструктор класса как единственный параметр.
- Могут возвращать новую функцию‑конструктор, заменяющую оригинальный класс.
- Выполняются снизу вверх (сначала самый внутренний декоратор).
- Используются для логирования, запечатывания, замораживания или добавления метаданных.
Декораторы методов
Декораторы методов применяются к объявлениям методов и позволяют наблюдать за ними, модифицировать или заменять их определения.
Они получают три параметра:
target: Прототип класса (для методов экземпляра) или функция‑конструктор (для статических методов)propertyKey: Имя методаdescriptor: Дескриптор свойства для метода
Декоратор измерения времени выполнения метода
Этот декоратор замеряет и логирует время выполнения метода:
// Декоратор метода для измерения времени выполнения
function measureTime(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} выполнен за ${(end - start).toFixed(2)} мс`);
return result;
};
return descriptor;
}
// Используем декоратор
class DataProcessor {
@measureTime
processData(data: number[]): number[] {
// Имитируем время обработки
for (let i = 0; i < 100000000; i++) { /* обработка */ }
return data.map(x => x * 2);
}
}
// При вызове будет выведено время выполнения
const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);
Декоратор контроля доступа по ролям
Пример реализации контроля доступа на основе ролей с помощью декораторов методов:
// Роли пользователей
type UserRole = 'admin' | 'editor' | 'viewer';
// Контекст текущего пользователя (упрощённо)
const currentUser = {
id: 1,
name: 'John Doe',
roles: ['viewer'] as UserRole[]
};
// Фабрика декораторов для контроля доступа по ролям
function AllowedRoles(...allowedRoles: UserRole[]) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const hasPermission = allowedRoles.some(role =>
currentUser.roles.includes(role)
);
if (!hasPermission) {
throw new Error(
`Пользователь ${currentUser.name} не авторизован для вызова ${propertyKey}`
);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
// Используем декоратор
class DocumentService {
@AllowedRoles('admin', 'editor')
deleteDocument(id: string) {
console.log(`Документ ${id} удалён`);
}
@AllowedRoles('admin', 'editor', 'viewer')
viewDocument(id: string) {
console.log(`Просмотр документа ${id}`);
}
}
// Использование
const docService = new DocumentService();
try {
docService.viewDocument('doc123'); // Работает — роль viewer разрешена
docService.deleteDocument('doc123'); // Выбрасывает ошибку — viewer не может удалять
} catch (error) {
console.error(error.message);
}
// Меняем роль пользователя на admin
currentUser.roles = ['admin'];
docService.deleteDocument('doc123'); // Теперь работает — admin может удалять
Декоратор предупреждения об устаревании
Этот декоратор добавляет предупреждение об устаревании метода, который будет удалён в будущей версии:
function deprecated(message: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.warn(`Предупреждение: метод ${propertyKey} устарел. ${message}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class PaymentService {
@deprecated('Используйте processPaymentV2 вместо этого')
processPayment(amount: number, currency: string) {
console.log(`Обработка платежа на сумму ${amount} ${currency}`);
}
processPaymentV2(amount: number, currency: string) {
console.log(`Обработка платежа v2 на сумму ${amount} ${currency}`);
}
}
const payment = new PaymentService();
payment.processPayment(100, 'USD'); // Показывает предупреждение об устаревании
payment.processPaymentV2(100, 'USD'); // Предупреждения нет
Ключевые моменты о декораторах методов
- Декораторы методов вызываются при определении метода, а не при его вызове.
- Они могут изменять поведение метода, оборачивая его дополнительной логикой.
- Используются для сквозных задач: логирования, валидации, авторизации.
- Получают дескриптор свойства метода — это позволяет модифицировать его поведение.
- Должны возвращать дескриптор свойства или
undefined(если не изменяют дескриптор).
Декораторы свойств
Декораторы свойств применяются к объявлениям свойств и позволяют наблюдать за ними, модифицировать или заменять их определения.
Они получают два параметра:
target: Прототип класса (для свойств экземпляра) или функция‑конструктор (для статических свойств)propertyKey: Имя свойства
Декоратор форматирования свойства
Этот декоратор автоматически форматирует свойство при его установке:
// Декоратор свойства для форматирования строкового значения
function format(formatString: string) {
return function (target: any, propertyKey: string) {
let value: string;
const getter = () => value;
const setter = (newVal: string) => {
value = formatString.replace('{}', newVal);
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Greeter {
@format('Hello, {}!')
greeting: string;
}
const greeter = new Greeter();
greeter.greeting = 'World';
console.log(greeter.greeting); // Выводит: Hello, World!
Декоратор логирования доступа к свойству
Этот декоратор логирует доступ к свойству и его изменения:
function logProperty(target: any, propertyKey: string) {
let value: any;
const getter = function() {
console.log(`Получаем ${propertyKey}: ${value}`);
return value;
};
const setter = function(newVal: any) {
console.log(`Устанавливаем ${propertyKey} со значения ${value} на ${newVal}`);
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Product {
@logProperty
name: string;
@logProperty
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
const product = new Product('Laptop', 999.99);
product.price = 899.99; // Логирует: Устанавливаем price со значения 999.99 на 899.99
console.log(product.name); // Логирует: Получаем name: Laptop
Декоратор обязательного свойства
Этот декоратор гарантирует, что свойство обязательно должно быть установлено:
function required(target: any, propertyKey: string) {
let value: any;
const getter = function() {
if (value === undefined) {
throw new Error(`Свойство ${propertyKey} обязательно к заполнению`);
}
return value;
};
const setter = function(newVal: any) {
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@required
username: string;
@required
email: string;
age?: number;
constructor(username: string, email: string) {
this.username = username;
this.email = email;
}
}
const user1 = new User('johndoe', 'john@example.com'); // Работает
// const user2 = new User(undefined, 'test@example.com'); // Ошибка: Свойство username обязательно
// const user2 = new User('johndoe', undefined); // Ошибка: Свойство email обязательно
Ключевые моменты о декораторах свойств
- Декораторы свойств вызываются при определении свойства, а не при его использовании.
- В отличие от декораторов методов, они не получают дескриптор свойства.
- Для изменения поведения свойства нужно использовать
Object.defineProperty. - Часто применяются для отражения метаданных или модификации доступа к свойствам.
- Могут комбинироваться с другими декораторами для более сложных сценариев.
Декораторы параметров
Декораторы параметров применяются к объявлениям параметров в конструкторе или методе.
Они получают три параметра:
target: Прототип класса (для методов экземпляра) или функция‑конструктор (для статических методов)propertyKey: Имя метода (илиundefinedдля параметров конструктора)parameterIndex: Порядковый индекс параметра в списке параметров функции
Декоратор валидации параметров
Этот декоратор проверяет параметры метода:
function validateParam(type: 'string' | 'number' | 'boolean') {
return function (target: any, propertyKey: string | symbol, parameterIndex: number) {
const existingValidations: any[] = Reflect.getOwnMetadata('validations', target, propertyKey) || [];
existingValidations.push({ index: parameterIndex, type });
Reflect.defineMetadata('validations', existingValidations, target, propertyKey);
};
}
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const validations: Array<{index: number, type: string}> =
Reflect.getOwnMetadata('validations', target, propertyKey) || [];
for (const validation of validations) {
const { index, type } = validation;
const param = args[index];
let isValid = false;
switch (type) {
case 'string':
isValid = typeof param === 'string' && param.length > 0;
break;
case 'number':
isValid = typeof param === 'number' && !isNaN(param);
break;
case 'boolean':
isValid = typeof param === 'boolean';
}
if (!isValid) {
throw new Error(`Параметр под индексом ${index} не прошёл валидацию типа ${type}`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class UserService {
@validate
createUser(
@validateParam('string') name: string,
@validateParam('number') age: number,
@validateParam('boolean') isActive: boolean
) {
console.log(`Создаём пользователя: ${name}, ${age}, ${isActive}`);
}
}
const service = new UserService();
service.createUser('John', 30, true); // Работает
// service.createUser('', 30, true); // Ошибка: Параметр под индексом 0 не прошёл валидацию строки
// service.createUser('John', 'thirty', true); // Ошибка: Параметр под индексом 1 не прошёл валидацию числа
Ключевые моменты о декораторах параметров
- Декораторы параметров вызываются при определении метода, а не при его вызове.
- Часто применяются совместно с декораторами методов для реализации сквозных задач.
- Могут использоваться вместе с библиотекой
reflect-metadataдля сохранения и извлечения метаданных. - Широко распространены во фреймворках внедрения зависимостей.
- Получают индекс параметра — это позволяет обращаться к значению параметра во время выполнения.
Фабрики декораторов
Фабрики декораторов — это функции, возвращающие функцию‑декоратор.
Они позволяют настраивать декораторы через передачу параметров, что делает их более гибкими и пригодными для повторного использования.
Настраиваемый декоратор логирования
Пример создания настраиваемого декоратора логирования с возможностью выбора уровня логирования:
// Фабрика декораторов с поддержкой конфигурации
function logWithConfig(config: { level: 'log' | 'warn' | 'error', message?: string }) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const { level = 'log', message = 'Выполнение метода' } = config;
console[level](`${message}: ${propertyKey}`, { arguments: args });
const result = originalMethod.apply(this, args);
console[level](`${propertyKey} завершён`);
return result;
};
return descriptor;
};
}
class PaymentService {
@logWithConfig({ level: 'log', message: 'Обработка платежа' })
processPayment(amount: number) {
console.log(`Обработка платежа на сумму $${amount}`);
}
}
Порядок выполнения декораторов
Пример, демонстрирующий порядок выполнения при применении нескольких декораторов:
Когда к объявлению применяется несколько декораторов, они выполняются в следующем порядке:
- Декораторы параметров → затем декораторы методов/аксессоров/свойств — для каждого члена экземпляра.
- Декораторы параметров → затем декораторы методов/аксессоров/свойств — для каждого статического члена.
- Декораторы параметров для конструктора.
- Декораторы классов для класса.
function first() {
console.log('first(): фабрика отработала');
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('first(): вызван');
};
}
function second() {
console.log('second(): фабрика отработала');
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('second(): вызван');
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// Вывод:
// second(): фабрика отработала
// first(): фабрика отработала
// first(): вызван
// second(): вызван
Реальные примеры
API‑контроллер с декораторами
Пример использования декораторов для создания простого API‑контроллера (по аналогии с NestJS или Express):
// Упрощённые реализации декораторов (для примера)
const ROUTES: any[] = [];
function Controller(prefix: string = '') {
return function (constructor: Function) {
constructor.prototype.prefix = prefix;
};
}
function Get(path: string = '') {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
ROUTES.push({
method: 'get',
path,
handler: descriptor.value,
target: target.constructor
});
};
}
// Использование декораторов
@Controller('/users')
class UserController {
@Get('/')
getAllUsers() {
return { users: [{ id: 1, name: 'John' }] };
}
@Get('/:id')
getUserById(id: string) {
return { id, name: 'John' };
}
}
// Имитация регистрации маршрутов
function registerRoutes() {
ROUTES.forEach(route => {
const prefix = route.target.prototype.prefix || '';
console.log(`Зарегистрирован ${route.method.toUpperCase()} ${prefix}${route.path}`);
});
}
registerRoutes();
// Вывод:
// Зарегистрирован GET /users
// Зарегистрирован GET /users/:id
Лучшие практики
Соблюдайте следующие правила при работе с декораторами:
- Фокусируйтесь на одной задаче: Каждый декоратор должен выполнять одну задачу.
- Документируйте поведение: Чётко описывайте, что делает декоратор и какие побочные эффекты возможны.
- Используйте фабрики декораторов: Используйте фабрики декораторов для их настройки — это повышает повторное использование декораторов.
- Производительность: Учитывайте воздействие на производительность, особенно в критичных по скорости работы участках кода.
- Типобезопасность: По возможности используйте систему типов TypeScript для проверки декораторов.
- Обработка ошибок: Реализуйте корректную обработку исключений внутри декораторов.
- Тестирование: Пишите юнит‑тесты для декораторов, чтобы убедиться в их корректной работе.
- Метаданные: Применяйте библиотеку
reflect-metadataв сложных сценариях, требующих информации о типах во время выполнения.
Типичные ошибки
Избегайте следующих ошибок:
- Не включена поддержка декораторов: Убедитесь, что в
tsconfig.jsonзадана опцияexperimentalDecorators: true. - Неверная сигнатура декоратора: У каждого типа декораторов своя сигнатура — ошибки в параметрах приводят к сбоям во время выполнения.
- Порядок выполнения: Декораторы выполняются снизу вверх для каждого объявления.
- Инициализация свойств: Декораторы свойств выполняются до инициализации свойств экземпляра.
- Отображение метаданных: Не забудьте импортировать библиотеку
reflect-metadata, если используете метаданные декораторов. - Нагрузка на производительность: Будьте осторожны с декораторами, добавляющими значительные задержки в коде, для которого критична скорость выполнения.
- Совместимость с браузерами: Декораторы — предложение стадии 3, могут требовать транспиляции для старых браузеров.
Пример: декоратор свойства
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
class Person {
@readonly
name = "John";
}
Пример: декоратор параметров
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Параметр в методе ${propertyKey} под индексом ${parameterIndex}`);
}
class Demo {
greet(@logParameter message: string) {
return message;
}
}
Чтобы активировать декораторы, добавьте в tsconfig.json следующую настройку:
Пример
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Где используются декораторы?
- Angular: для компонентов, сервисов, модулей и т. д.
- NestJS: для контроллеров, провайдеров, маршрутов и т. д.