Русский

Изучите возможности декораторов TypeScript для программирования на основе метаданных, аспектно-ориентированного программирования и улучшения кода с помощью декларативных паттернов. Комплексное руководство для разработчиков.

Декораторы TypeScript: освоение паттернов программирования на основе метаданных для создания надёжных приложений

В обширном мире современной разработки программного обеспечения поддержание чистых, масштабируемых и управляемых кодовых баз имеет первостепенное значение. TypeScript, с его мощной системой типов и продвинутыми возможностями, предоставляет разработчикам инструменты для достижения этой цели. Одной из самых интригующих и преобразующих его функций являются декораторы. Хотя на момент написания статьи это всё ещё экспериментальная функция (предложение Stage 3 для ECMAScript), декораторы широко используются в таких фреймворках, как Angular и TypeORM, коренным образом меняя наш подход к паттернам проектирования, программированию на основе метаданных и аспектно-ориентированному программированию (АОП).

Это всеобъемлющее руководство углублённо рассмотрит декораторы TypeScript, исследуя их механику, различные типы, практическое применение и лучшие практики. Независимо от того, создаёте ли вы крупномасштабные корпоративные приложения, микросервисы или клиентские веб-интерфейсы, понимание декораторов позволит вам писать более декларативный, поддерживаемый и мощный код на TypeScript.

Понимание основной концепции: что такое декоратор?

По своей сути, декоратор — это особый вид объявления, который можно прикрепить к объявлению класса, метода, аксессора, свойства или параметра. Декораторы — это функции, которые возвращают новое значение (или изменяют существующее) для цели, которую они декорируют. Их основная цель — добавлять метаданные или изменять поведение объявления, к которому они прикреплены, не изменяя напрямую базовую структуру кода. Этот внешний, декларативный способ расширения кода невероятно мощен.

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

Синтаксис декоратора

Декораторы начинаются с символа @, за которым следует имя функции-декоратора. Они размещаются непосредственно перед объявлением, которое они декорируют.

@MyDecorator\nclass MyClass {\n  @AnotherDecorator\n  myMethod() {\n    // ...\n  }\n}

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

Прежде чем вы сможете использовать декораторы, вы должны включить опцию компилятора experimentalDecorators в вашем файле tsconfig.json. Кроме того, для расширенных возможностей рефлексии метаданных (часто используемых фреймворками) вам также понадобятся emitDecoratorMetadata и полифил reflect-metadata.

// tsconfig.json\n{\n  "compilerOptions": {\n    "target": "ES2017",\n    "module": "commonjs",\n    "experimentalDecorators": true,\n    "emitDecoratorMetadata": true,\n    "outDir": "./dist",\n    "strict": true,\n    "esModuleInterop": true,\n    "skipLibCheck": true,\n    "forceConsistentCasingInFileNames": true\n  }\n}

Вам также необходимо установить reflect-metadata:

npm install reflect-metadata --save\n# или\nyarn add reflect-metadata

И импортировать его в самом начале входной точки вашего приложения (например, main.ts или app.ts):

import "reflect-metadata";\n// Далее следует код вашего приложения

Фабрики декораторов: гибкая настройка

Хотя базовый декоратор — это функция, часто вам потребуется передавать аргументы в декоратор для настройки его поведения. Это достигается с помощью фабрики декораторов. Фабрика декораторов — это функция, которая возвращает саму функцию-декоратор. Когда вы применяете фабрику декораторов, вы вызываете её с аргументами, и она возвращает функцию-декоратор, которую TypeScript применяет к вашему коду.

Пример создания простой фабрики декораторов

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

function Logger(prefix: string) {\n  return function (target: Function) {\n    console.log(`[${prefix}] Class ${target.name} has been defined.`);\n  };\n}\n\n@Logger("APP_INIT")\nclass ApplicationBootstrap {\n  constructor() {\n    console.log("Application is starting...");\n  }\n}\n\nconst app = new ApplicationBootstrap();\n// Вывод:\n// [APP_INIT] Class ApplicationBootstrap has been defined.\n// Application is starting...

В этом примере Logger("APP_INIT") — это вызов фабрики декораторов. Он возвращает саму функцию-декоратор, которая принимает target: Function (конструктор класса) в качестве своего аргумента. Это позволяет динамически настраивать поведение декоратора.

Типы декораторов в TypeScript

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

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

Декораторы классов применяются к объявлениям классов. Функция-декоратор получает конструктор класса в качестве своего единственного аргумента. Декоратор класса может наблюдать, изменять или даже заменять определение класса.

Сигнатура:

function ClassDecorator(target: Function) { ... }

Возвращаемое значение:

Если декоратор класса возвращает значение, оно заменит объявление класса предоставленной функцией-конструктором. Это мощная возможность, часто используемая для миксинов или расширения классов. Если значение не возвращается, используется исходный класс.

Сценарии использования:

Пример декоратора класса: внедрение сервиса

Представьте простой сценарий внедрения зависимостей, где вы хотите пометить класс как "внедряемый" и опционально предоставить для него имя в контейнере.

const InjectableServiceRegistry = new Map<string, Function>();\n\nfunction Injectable(name?: string) {\n  return function<T extends { new(...args: any[]): {} }>(constructor: T) {\n    const serviceName = name || constructor.name;\n    InjectableServiceRegistry.set(serviceName, constructor);\n    console.log(`Registered service: ${serviceName}`);\n\n    // Опционально, здесь можно вернуть новый класс для расширения поведения\n    return class extends constructor {\n      createdAt = new Date();\n      // Дополнительные свойства или методы для всех внедряемых сервисов\n    };\n  };\n}\n\n@Injectable("UserService")\nclass UserDataService {\n  getUsers() {\n    return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];\n  }\n}\n\n@Injectable()\nclass ProductDataService {\n  getProducts() {\n    return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];\n  }\n}\n\nconsole.log("--- Services Registered ---");\nconsole.log(Array.from(InjectableServiceRegistry.keys()));\n\nconst userServiceConstructor = InjectableServiceRegistry.get("UserService");\nif (userServiceConstructor) {\n  const userServiceInstance = new userServiceConstructor();\n  console.log("Users:", userServiceInstance.getUsers());\n  // console.log("User Service Created At:", userServiceInstance.createdAt); // Если используется возвращённый класс\n}

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

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

Декораторы методов применяются к объявлениям методов. Они получают три аргумента: целевой объект (для статических членов — функция-конструктор; для членов экземпляра — прототип класса), имя метода и дескриптор свойства метода.

Сигнатура:

function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Возвращаемое значение:

Декоратор метода может возвращать новый PropertyDescriptor. Если он это делает, этот дескриптор будет использован для определения метода. Это позволяет вам изменять или заменять реализацию исходного метода, что делает его невероятно мощным для АОП.

Сценарии использования:

Пример декоратора метода: мониторинг производительности

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

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n\n  descriptor.value = function(...args: any[]) {\n    const start = process.hrtime.bigint();\n    const result = originalMethod.apply(this, args);\n    const end = process.hrtime.bigint();\n    const duration = Number(end - start) / 1_000_000;\n    console.log(`Method \"${propertyKey}\" executed in ${duration.toFixed(2)} ms`);\n    return result;\n  };\n\n  return descriptor;\n}\n\nclass DataProcessor {\n  @MeasurePerformance\n  processData(data: number[]): number[] {\n    // Симулируем сложную, ресурсоёмкую операцию\n    for (let i = 0; i < 1_000_000; i++) {\n      Math.sin(i);\n    }\n    return data.map(n => n * 2);\n  }\n\n  @MeasurePerformance\n  fetchRemoteData(id: string): Promise<string> {\n    return new Promise(resolve => {\n      setTimeout(() => {\n        resolve(`Data for ID: ${id}`);\n      }, 500);\n    });\n  }\n}\n\nconst processor = new DataProcessor();\nprocessor.processData([1, 2, 3]);\nprocessor.fetchRemoteData("abc").then(result => console.log(result));

Декоратор MeasurePerformance оборачивает исходный метод логикой замера времени, выводя продолжительность выполнения, не загромождая бизнес-логику внутри самого метода. Это классический пример аспектно-ориентированного программирования (АОП).

3. Декораторы аксессоров

Декораторы аксессоров применяются к объявлениям аксессоров (get и set). Подобно декораторам методов, они получают целевой объект, имя аксессора и его дескриптор свойства.

Сигнатура:

function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Возвращаемое значение:

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

Сценарии использования:

Пример декоратора аксессора: кэширование геттеров

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

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalGetter = descriptor.get;\n  const cacheKey = `_cached_${String(propertyKey)}`;\n\n  if (originalGetter) {\n    descriptor.get = function() {\n      if (this[cacheKey] === undefined) {\n        console.log(`[Cache Miss] Computing value for ${String(propertyKey)}`);\n        this[cacheKey] = originalGetter.apply(this);\n      } else {\n        console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);\n      }\n      return this[cacheKey];\n    };\n  }\n  return descriptor;\n}\n\nclass ReportGenerator {\n  private data: number[];\n\n  constructor(data: number[]) {\n    this.data = data;\n  }\n\n  // Симулирует ресурсоёмкое вычисление\n  @CachedGetter\n  get expensiveSummary(): number {\n    console.log("Performing expensive summary calculation...");\n    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;\n  }\n}\n\nconst generator = new ReportGenerator([10, 20, 30, 40, 50]);\n\nconsole.log("First access:", generator.expensiveSummary);\nconsole.log("Second access:", generator.expensiveSummary);\nconsole.log("Third access:", generator.expensiveSummary);

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

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

Декораторы свойств применяются к объявлениям свойств. Они получают два аргумента: целевой объект (для статических членов — функция-конструктор; для членов экземпляра — прототип класса) и имя свойства.

Сигнатура:

function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }

Возвращаемое значение:

Декораторы свойств не могут возвращать никакого значения. Их основное применение — регистрация метаданных о свойстве. Они не могут напрямую изменять значение свойства или его дескриптор во время декорирования, так как дескриптор для свойства ещё не полностью определён, когда выполняются декораторы свойств.

Сценарии использования:

Пример декоратора свойства: валидация обязательного поля

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

interface ValidationRule {\n  property: string | symbol;\n  validate: (value: any) => boolean;\n  message: string;\n}\n\nconst validationRules: Map<Function, ValidationRule[]> = new Map();\n\nfunction Required(target: Object, propertyKey: string | symbol) {\n  const rules = validationRules.get(target.constructor) || [];\n  rules.push({\n    property: propertyKey,\n    validate: (value: any) => value !== null && value !== undefined && value !== "",\n    message: `${String(propertyKey)} is required.`\n  });\n  validationRules.set(target.constructor, rules);\n}\n\nfunction validate(instance: any): string[] {\n  const classRules = validationRules.get(instance.constructor) || [];\n  const errors: string[] = [];\n\n  for (const rule of classRules) {\n    if (!rule.validate(instance[rule.property])) {\n      errors.push(rule.message);\n    }\n  }\n  return errors;\n}\n\nclass UserProfile {\n  @Required\n  firstName: string;\n\n  @Required\n  lastName: string;\n\n  age?: number;\n\n  constructor(firstName: string, lastName: string, age?: number) {\n    this.firstName = firstName;\n    this.lastName = lastName;\n    this.age = age;\n  }\n}\n\nconst user1 = new UserProfile("John", "Doe", 30);\nconsole.log("User 1 validation errors:", validate(user1)); // []\n\nconst user2 = new UserProfile("", "Smith");\nconsole.log("User 2 validation errors:", validate(user2)); // ["firstName is required."]\n\nconst user3 = new UserProfile("Alice", "");\nconsole.log("User 3 validation errors:", validate(user3)); // ["lastName is required."]

Декоратор Required просто регистрирует правило валидации в центральной карте validationRules. Отдельная функция validate затем использует эти метаданные для проверки экземпляра во время выполнения. Этот паттерн отделяет логику валидации от определения данных, делая её переиспользуемой и чистой.

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

Декораторы параметров применяются к параметрам в конструкторе класса или методе. Они получают три аргумента: целевой объект (для статических членов — функция-конструктор; для членов экземпляра — прототип класса), имя метода (или undefined для параметров конструктора) и порядковый индекс параметра в списке параметров функции.

Сигнатура:

function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

Возвращаемое значение:

Декораторы параметров не могут возвращать никакого значения. Подобно декораторам свойств, их основная роль — добавлять метаданные о параметре.

Сценарии использования:

Пример декоратора параметра: внедрение данных из запроса

Давайте симулируем, как веб-фреймворк может использовать декораторы параметров для внедрения конкретных данных в параметр метода, например, ID пользователя из запроса.

interface ParameterMetadata {\n  index: number;\n  key: string | symbol;\n  resolver: (request: any) => any;\n}\n\nconst parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();\n\nfunction RequestParam(paramName: string) {\n  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {\n    const targetKey = propertyKey || "constructor";\n    let methodResolvers = parameterResolvers.get(target.constructor);\n    if (!methodResolvers) {\n      methodResolvers = new Map();\n      parameterResolvers.set(target.constructor, methodResolvers);\n    }\n    const paramMetadata = methodResolvers.get(targetKey) || [];\n    paramMetadata.push({\n      index: parameterIndex,\n      key: targetKey,\n      resolver: (request: any) => request[paramName]\n    });\n    methodResolvers.set(targetKey, paramMetadata);\n  };\n}\n\n// Гипотетическая функция фреймворка для вызова метода с разрешёнными параметрами\nfunction executeWithParams(instance: any, methodName: string, request: any) {\n  const classResolvers = parameterResolvers.get(instance.constructor);\n  if (!classResolvers) {\n    return (instance[methodName] as Function).apply(instance, []);\n  }\n  const methodParamMetadata = classResolvers.get(methodName);\n  if (!methodParamMetadata) {\n    return (instance[methodName] as Function).apply(instance, []);\n  }\n\n  const args: any[] = Array(methodParamMetadata.length);\n  for (const meta of methodParamMetadata) {\n    args[meta.index] = meta.resolver(request);\n  }\n  return (instance[methodName] as Function).apply(instance, args);\n}\n\nclass UserController {\n  getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {\n    console.log(`Fetching user with ID: ${userId}, Token: ${authToken || "N/A"}`);\n    return { id: userId, name: "Jane Doe" };\n  }\n\n  deleteUser(@RequestParam("id") userId: string) {\n    console.log(`Deleting user with ID: ${userId}`);\n    return { status: "deleted", id: userId };\n  }\n}\n\nconst userController = new UserController();\n\n// Симулируем входящий запрос\nconst mockRequest = {\n  id: "user123",\n  token: "abc-123",\n  someOtherProp: "xyz"\n};\n\nconsole.log("\n--- Executing getUser ---");\nexecuteWithParams(userController, "getUser", mockRequest);\n\nconsole.log("\n--- Executing deleteUser ---");\nexecuteWithParams(userController, "deleteUser", { id: "user456" });

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

Композиция декораторов и порядок выполнения

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

Несколько декораторов на одном элементе

Когда к одному объявлению (например, классу, методу или свойству) применяется несколько декораторов, они выполняются в определённом порядке: снизу вверх или справа налево для их оценки. Однако их результаты применяются в обратном порядке.

@DecoratorA\n@DecoratorB\nclass MyClass {\n  // ...\n}\n

Здесь сначала будет оценён DecoratorB, затем DecoratorA. Если они изменяют класс (например, возвращая новый конструктор), изменение от DecoratorA обернёт или применится поверх изменения от DecoratorB.

Пример: цепочка декораторов методов

Рассмотрим два декоратора методов: LogCall и Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n  descriptor.value = function (...args: any[]) {\n    console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);\n    const result = originalMethod.apply(this, args);\n    console.log(`[LOG] Method ${String(propertyKey)} returned:`, result);\n    return result;\n  };\n  return descriptor;\n}\n\nfunction Authorization(roles: string[]) {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value;\n    descriptor.value = function (...args: any[]) {\n      const currentUserRoles = ["admin"]; // Симулируем получение ролей текущего пользователя\n      const authorized = roles.some(role => currentUserRoles.includes(role));\n      if (!authorized) {\n        console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(", ")}`);\n        throw new Error("Unauthorized access");\n      }\n      console.log(`[AUTH] Access granted for ${String(propertyKey)}`);\n      return originalMethod.apply(this, args);\n    };\n    return descriptor;\n  };\n}\n\nclass SecureService {\n  @LogCall\n  @Authorization(["admin"])\n  deleteSensitiveData(id: string) {\n    console.log(`Deleting sensitive data for ID: ${id}`);\n    return `Data ID ${id} deleted.`;\n  }\n\n  @Authorization(["user"])\n  @LogCall // Здесь изменён порядок\n  fetchPublicData(query: string) {\n    console.log(`Fetching public data with query: ${query}`);\n    return `Public data for query: ${query}`; \n  }\n}\n\nconst service = new SecureService();\n\ntry {\n  console.log("\n--- Calling deleteSensitiveData (Admin User) ---");\n  service.deleteSensitiveData("record123");\n} catch (error: any) {\n  console.error(error.message);\n}\n\ntry {\n  console.log("\n--- Calling fetchPublicData (Non-Admin User) ---");\n  // Симулируем попытку доступа не-администратора к fetchPublicData, который требует роль 'user'\n  const mockUserRoles = ["guest"]; // Это приведёт к ошибке авторизации\n  // Чтобы сделать это динамическим, вам понадобится система DI или статический контекст для ролей текущего пользователя.\n  // Для простоты, мы предполагаем, что декоратор Authorization имеет доступ к контексту текущего пользователя.\n  // Давайте изменим декоратор Authorization, чтобы он всегда предполагал роль 'admin' для демонстрации,\n  // чтобы первый вызов был успешным, а второй — неудачным, для показа разных путей.\n  \n  // Повторный запуск с ролью 'user' для успешного выполнения fetchPublicData.\n  // Представьте, что currentUserRoles в Authorization становится: ['user']\n  // В этом примере, давайте упростим и покажем эффект порядка.\n  service.fetchPublicData("search term"); // Это выполнит Auth -> Log\n} catch (error: any) {\n  console.error(error.message);\n}\n\n/* Ожидаемый вывод для deleteSensitiveData:\n[AUTH] Access granted for deleteSensitiveData\n[LOG] Calling deleteSensitiveData with args: [ 'record123' ]\nDeleting sensitive data for ID: record123\n[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.\n*/\n\n/* Ожидаемый вывод для fetchPublicData (если у пользователя есть роль 'user'):\n[LOG] Calling fetchPublicData with args: [ 'search term' ]\n[AUTH] Access granted for fetchPublicData\nFetching public data with query: search term\n[LOG] Method fetchPublicData returned: Public data for query: search term\n*/

Обратите внимание на порядок: для deleteSensitiveData сначала выполняется Authorization (нижний), а затем LogCall (верхний) оборачивает его. Внутренняя логика Authorization выполняется первой. Для fetchPublicData сначала выполняется LogCall (нижний), а затем Authorization (верхний) оборачивает его. Это означает, что аспект LogCall будет находиться снаружи аспекта Authorization. Эта разница критична для сквозных задач, таких как логирование или обработка ошибок, где порядок выполнения может значительно влиять на поведение.

Порядок выполнения для разных элементов

Когда класс, его члены и параметры имеют декораторы, порядок выполнения чётко определён:

  1. Декораторы параметров применяются первыми, для каждого параметра, начиная с последнего и заканчивая первым.
  2. Затем применяются декораторы методов, аксессоров или свойств для каждого члена.
  3. Наконец, декораторы классов применяются к самому классу.

Внутри каждой категории несколько декораторов на одном и том же элементе применяются снизу вверх (или справа налево).

Пример: полный порядок выполнения

function log(message: string) {\n  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {\n    if (typeof descriptorOrIndex === 'number') {\n      console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || "constructor")}`);\n    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {\n      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {\n        console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);\n      } else {\n        console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);\n      }\n    } else {\n      console.log(`Class Decorator: ${message} on ${target.name}`);\n    }\n    return descriptorOrIndex; // Возвращаем дескриптор для метода/аксессора, undefined для остального\n  };\n}\n\n@log("Class Level D")\n@log("Class Level C")\nclass MyDecoratedClass {\n  @log("Static Property A")\n  static staticProp: string = "";\n\n  @log("Instance Property B")\n  instanceProp: number = 0;\n\n  @log("Method D")\n  @log("Method C")\n  myMethod(\n    @log("Parameter Z") paramZ: string,\n    @log("Parameter Y") paramY: number\n  ) {\n    console.log("Method myMethod executed.");\n  }\n\n  @log("Getter/Setter F")\n  get myAccessor() {\n    return "";\n  }\n\n  set myAccessor(value: string) {\n    //...\n  }\n\n  constructor() {\n    console.log("Constructor executed.");\n  }\n}\n\nnew MyDecoratedClass();\n// Вызываем метод, чтобы сработал декоратор метода\nnew MyDecoratedClass().myMethod("hello", 123);\n\n/* Предполагаемый порядок вывода (приблизительный, зависит от конкретной версии TypeScript и компиляции):\nParam Decorator: Parameter Y on parameter #1 of myMethod\nParam Decorator: Parameter Z on parameter #0 of myMethod\nProperty Decorator: Static Property A on staticProp\nProperty Decorator: Instance Property B on instanceProp\nMethod/Accessor Decorator: Getter/Setter F on myAccessor\nMethod/Accessor Decorator: Method C on myMethod\nMethod/Accessor Decorator: Method D on myMethod\nClass Decorator: Class Level C on MyDecoratedClass\nClass Decorator: Class Level D on MyDecoratedClass\nConstructor executed.\nMethod myMethod executed.\n*/

Точное время вывода в консоль может незначительно варьироваться в зависимости от того, когда вызывается конструктор или метод, но порядок, в котором выполняются сами функции-декораторы (и, следовательно, применяются их побочные эффекты или возвращаемые значения), следует вышеуказанным правилам.

Практическое применение и паттерны проектирования с декораторами

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

1. Внедрение зависимостей (DI)

Одно из самых заметных применений декораторов — в фреймворках внедрения зависимостей (таких как @Injectable(), @Component() и т. д. в Angular или широкое использование DI в NestJS). Декораторы позволяют объявлять зависимости непосредственно в конструкторах или свойствах, позволяя фреймворку автоматически создавать и предоставлять правильные сервисы.

Пример: упрощенное внедрение сервисов

import "reflect-metadata"; // Обязательно для emitDecoratorMetadata\n\nconst INJECTABLE_METADATA_KEY = Symbol("injectable");\nconst INJECT_METADATA_KEY = Symbol("inject");\n\nfunction Injectable() {\n  return function (target: Function) {\n    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);\n  };\n}\n\nfunction Inject(token: any) {\n  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\n    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];\n    existingInjections[parameterIndex] = token;\n    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);\n  };\n}\n\nclass Container {\n  private static instances = new Map<any, any>();\n\n  static resolve<T>(target: { new (...args: any[]): T }): T {\n    if (Container.instances.has(target)) {\n      return Container.instances.get(target);\n    }\n\n    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);\n    if (!isInjectable) {\n      throw new Error(`Class ${target.name} is not marked as @Injectable.`);\n    }\n
    // Получаем типы параметров конструктора (требует emitDecoratorMetadata)\n    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];\n    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];\n\n    const dependencies = paramTypes.map((paramType, index) => {\n      // Используем явный токен @Inject, если он предоставлен, иначе выводим тип\n      const token = explicitInjections[index] || paramType;\n      if (token === undefined) {\n        throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);\n      }\n      return Container.resolve(token);\n    });\n\n    const instance = new target(...dependencies);\n    Container.instances.set(target, instance);\n    return instance;\n  }\n}\n\n// Определяем сервисы\n@Injectable()\nclass DatabaseService {\n  connect() {\n    console.log("Connecting to database...");\n    return "DB Connection";\n  }\n}\n\n@Injectable()\nclass AuthService {\n  private db: DatabaseService;\n\n  constructor(db: DatabaseService) {\n    this.db = db;\n  }\n\n  login() {\n    console.log(`AuthService: Authenticating using ${this.db.connect()}`);\n    return "User logged in";\n  }\n}\n\n@Injectable()\nclass UserService {\n  private authService: AuthService;\n  private dbService: DatabaseService; // Пример внедрения через свойство с использованием кастомного декоратора или функции фреймворка\n\n  constructor(@Inject(AuthService) authService: AuthService,\n              @Inject(DatabaseService) dbService: DatabaseService) {\n    this.authService = authService;\n    this.dbService = dbService;\n  }\n\n  getUserProfile() {\n    this.authService.login();\n    this.dbService.connect();\n    console.log("UserService: Fetching user profile...");\n    return { id: 1, name: "Global User" };\n  }\n}\n\n// Разрешаем основной сервис\nconsole.log("--- Resolving UserService ---");\nconst userService = Container.resolve(UserService);\nconsole.log(userService.getUserProfile());\n\nconsole.log("\n--- Resolving AuthService (should be cached) ---");\nconst authService = Container.resolve(AuthService);\nauthService.login();

Этот сложный пример демонстрирует, как декораторы @Injectable и @Inject в сочетании с reflect-metadata позволяют кастомному Container автоматически разрешать и предоставлять зависимости. Метаданные design:paramtypes, автоматически генерируемые TypeScript (когда emitDecoratorMetadata равно true), здесь имеют решающее значение.

2. Аспектно-ориентированное программирование (АОП)

АОП фокусируется на модуляризации сквозных задач (например, логирование, безопасность, транзакции), которые затрагивают множество классов и модулей. Декораторы отлично подходят для реализации концепций АОП в TypeScript.

Пример: логирование с помощью декоратора метода

Возвращаясь к декоратору LogCall, это прекрасный пример АОП. Он добавляет логику логирования к любому методу, не изменяя исходный код метода. Это разделяет "что делать" (бизнес-логика) и "как это делать" (логирование, мониторинг производительности и т.д.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n  descriptor.value = function (...args: any[]) {\n    console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);\n    try {\n      const result = originalMethod.apply(this, args);\n      console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);\n      return result;\n    } catch (error: any) {\n      console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);\n      throw error;\n    }\n  };\n  return descriptor;\n}\n\nclass PaymentProcessor {\n  @LogMethod\n  processPayment(amount: number, currency: string) {\n    if (amount <= 0) {\n      throw new Error("Payment amount must be positive.");\n    }\n    console.log(`Processing payment of ${amount} ${currency}...`);\n    return `Payment of ${amount} ${currency} processed successfully.`;\n  }\n\n  @LogMethod\n  refundPayment(transactionId: string) {\n    console.log(`Refunding payment for transaction ID: ${transactionId}...`);\n    return `Refund initiated for ${transactionId}.`;\n  }\n}\n\nconst processor = new PaymentProcessor();\nprocessor.processPayment(100, "USD");\ntry {\n  processor.processPayment(-50, "EUR");\n} catch (error: any) {\n  console.error("Caught error:", error.message);\n}

Такой подход позволяет классу PaymentProcessor сосредоточиться исключительно на логике платежей, в то время как декоратор LogMethod обрабатывает сквозную задачу логирования.

3. Валидация и преобразование

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

Пример: валидация данных с помощью декораторов свойств

Пример @Required ранее уже демонстрировал это. Вот ещё один пример с валидацией числового диапазона.

interface FieldValidationRule {\n  property: string | symbol;\n  validator: (value: any) => boolean;\n  message: string;\n}\n\nconst fieldValidationRules = new Map<Function, FieldValidationRule[]>();\n\nfunction addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {\n  const rules = fieldValidationRules.get(target.constructor) || [];\n  rules.push({ property: propertyKey, validator, message });\n  fieldValidationRules.set(target.constructor, rules);\n}\n\nfunction IsPositive(target: Object, propertyKey: string | symbol) {\n  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} must be a positive number.`);\n}\n\nfunction MaxLength(maxLength: number) {\n  return function (target: Object, propertyKey: string | symbol) {\n    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);\n  };\n}\n\nclass Product {\n  @MaxLength(50)\n  name: string;\n\n  @IsPositive\n  price: number;\n\n  constructor(name: string, price: number) {\n    this.name = name;\n    this.price = price;\n  }\n\n  static validate(instance: any): string[] {\n    const errors: string[] = [];\n    const rules = fieldValidationRules.get(instance.constructor) || [];\n    for (const rule of rules) {\n      if (!rule.validator(instance[rule.property])) {\n        errors.push(rule.message);\n      }\n    }\n    return errors;\n  }\n}\n\nconst product1 = new Product("Laptop", 1200);\nconsole.log("Product 1 errors:", Product.validate(product1)); // []\n\nconst product2 = new Product("Very long product name that exceeds fifty characters limit for testing purpose", 50);\nconsole.log("Product 2 errors:", Product.validate(product2)); // ["name must be at most 50 characters long."]\n\nconst product3 = new Product("Book", -10);\nconsole.log("Product 3 errors:", Product.validate(product3)); // ["price must be a positive number."]

Такая настройка позволяет декларативно определять правила валидации для свойств вашей модели, делая ваши модели данных самоописывающимися в плане их ограничений.

Лучшие практики и рекомендации

Хотя декораторы мощны, их следует использовать разумно. Неправильное использование может привести к коду, который сложнее отлаживать или понимать.

Когда использовать декораторы (а когда нет)

Влияние на производительность

Декораторы выполняются во время компиляции (или во время определения в среде выполнения JavaScript, если транслируются). Преобразование или сбор метаданных происходит, когда определяется класс/метод, а не при каждом вызове. Поэтому влияние на производительность во время выполнения от *применения* декораторов минимально. Однако, *логика внутри* ваших декораторов может влиять на производительность, особенно если они выполняют дорогостоящие операции при каждом вызове метода (например, сложные вычисления внутри декоратора метода).

Поддерживаемость и читаемость

Декораторы, при правильном использовании, могут значительно улучшить читаемость, вынося шаблонный код из основной логики. Однако, если они выполняют сложные, скрытые преобразования, отладка может стать сложной. Убедитесь, что ваши декораторы хорошо документированы, и их поведение предсказуемо.

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

Важно повторить, что декораторы TypeScript основаны на предложении TC39 Stage 3. Это означает, что спецификация в основном стабильна, но всё ещё может претерпеть незначительные изменения, прежде чем станет частью официального стандарта ECMAScript. Фреймворки, такие как Angular, приняли их, делая ставку на их окончательную стандартизацию. Это подразумевает определённый уровень риска, хотя, учитывая их широкое распространение, значительные ломающие изменения маловероятны.

Предложение TC39 эволюционировало. Текущая реализация TypeScript основана на старой версии предложения. Существует различие между "Legacy Decorators" и "Standard Decorators". Когда официальный стандарт будет принят, TypeScript, вероятно, обновит свою реализацию. Для большинства разработчиков, использующих фреймворки, этот переход будет управляться самим фреймворком. Для авторов библиотек может стать необходимым понимание тонких различий между устаревшими и будущими стандартными декораторами.

Опция компилятора emitDecoratorMetadata

Эта опция, когда установлена в true в tsconfig.json, указывает компилятору TypeScript генерировать определённые метаданные типов времени разработки в скомпилированный JavaScript. Эти метаданные включают тип параметров конструктора (design:paramtypes), тип возвращаемого значения методов (design:returntype) и тип свойств (design:type).

Эти сгенерированные метаданные не являются частью стандартной среды выполнения JavaScript. Они обычно потребляются полифилом reflect-metadata, который затем делает их доступными через функции Reflect.getMetadata(). Это абсолютно критично для продвинутых паттернов, таких как внедрение зависимостей, где контейнеру необходимо знать типы зависимостей, которые требует класс, без явной конфигурации.

Продвинутые паттерны с декораторами

Декораторы можно комбинировать и расширять для создания ещё более сложных паттернов.

1. Декорирование декораторов (декораторы высшего порядка)

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

// Декоратор, который гарантирует, что метод логируется и требует прав администратора\nfunction AdminAndLoggedMethod() {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    // Сначала применяем Authorization (внутренний)\n    Authorization(["admin"])(target, propertyKey, descriptor);\n    // Затем применяем LogCall (внешний)\n    LogCall(target, propertyKey, descriptor);\n\n    return descriptor; // Возвращаем изменённый дескриптор\n  };\n}\n\nclass AdminPanel {\n  @AdminAndLoggedMethod()\n  deleteUserAccount(userId: string) {\n    console.log(`Deleting user account: ${userId}`);\n    return `User ${userId} deleted.`;\n  }\n}\n\nconst adminPanel = new AdminPanel();\nadminPanel.deleteUserAccount("user007");\n/* Ожидаемый вывод (при условии наличия роли администратора):\n[AUTH] Access granted for deleteUserAccount\n[LOG] Calling deleteUserAccount with args: [ 'user007' ]\nDeleting user account: user007\n[LOG] Method deleteUserAccount returned: User user007 deleted.\n*/

Здесь AdminAndLoggedMethod — это фабрика, которая возвращает декоратор, и внутри этого декоратора она применяет два других декоратора. Этот паттерн может инкапсулировать сложные композиции декораторов.

2. Использование декораторов для миксинов (примесей)

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

function ApplyMixins(constructors: Function[]) {\n  return function (derivedConstructor: Function) {\n    constructors.forEach(baseConstructor => {\n      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {\n        Object.defineProperty(\n          derivedConstructor.prototype,\n          name,\n          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)\n        );\n      });\n    });\n  };\n}\n\nclass Disposable {\n  isDisposed: boolean = false;\n  dispose() {\n    this.isDisposed = true;\n    console.log("Object disposed.");\n  }\n}\n\nclass Loggable {\n  log(message: string) {\n    console.log(`[Loggable] ${message}`);\n  }\n}\n\n@ApplyMixins([Disposable, Loggable])\nclass MyResource implements Disposable, Loggable {\n  // Эти свойства/методы внедряются декоратором\n  isDisposed!: boolean;\n  dispose!: () => void;\n  log!: (message: string) => void;\n\n  constructor(public name: string) {\n    this.log(`Resource ${this.name} created.`);\n  }\n\n  cleanUp() {\n    this.dispose();\n    this.log(`Resource ${this.name} cleaned up.`);\n  }\n}\n\nconst resource = new MyResource("NetworkConnection");\nconsole.log(`Is disposed: ${resource.isDisposed}`);\nresource.cleanUp();\nconsole.log(`Is disposed: ${resource.isDisposed}`);

Этот декоратор @ApplyMixins динамически копирует методы и свойства из базовых конструкторов в прототип производного класса, эффективно "подмешивая" функциональность.

Заключение: расширение возможностей современной разработки на TypeScript

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

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

Освоив декораторы TypeScript, вы получаете значительный инструмент в свой арсенал, позволяющий создавать более надёжные, масштабируемые и интеллектуальные приложения. Используйте их ответственно, понимайте их механику и открывайте новый уровень декларативной мощи в ваших проектах на TypeScript.