Română

Explorează puterea decoratorilor TypeScript pentru programare cu metadate și aspect-orientată. Îmbunătățește codul cu modele declarative. Ghid complet.

Decoratorii TypeScript: Stăpânirea Modelelor de Programare cu Metadate pentru Aplicații Robuste

În peisajul vast al dezvoltării software moderne, menținerea unor baze de cod curate, scalabile și ușor de gestionat este primordială. TypeScript, cu sistemul său puternic de tipuri și funcțiile avansate, oferă dezvoltatorilor instrumente pentru a realiza acest lucru. Printre cele mai interesante și transformative caracteristici ale sale se numără Decoratorii. Deși încă o caracteristică experimentală la momentul redactării (propunere de Etapa 3 pentru ECMAScript), decoratorii sunt utilizați pe scară largă în framework-uri precum Angular și TypeORM, schimbând fundamental modul în care abordăm modelele de design, programarea cu metadate și programarea orientată pe aspecte (AOP).

Acest ghid cuprinzător va aprofunda decoratorii TypeScript, explorând mecanismele lor, diferitele tipuri, aplicațiile practice și cele mai bune practici. Indiferent dacă construiți aplicații enterprise la scară largă, microservicii sau interfețe web client-side, înțelegerea decoratorilor vă va permite să scrieți un cod TypeScript mai declarativ, mai ușor de întreținut și mai puternic.

Înțelegerea Conceptului de Bază: Ce este un Decorator?

În esența sa, un decorator este un tip special de declarație care poate fi atașat unei declarații de clasă, metode, accesor, proprietăți sau parametru. Decoratorii sunt funcții care returnează o nouă valoare (sau modifică una existentă) pentru ținta pe care o decorează. Scopul lor principal este de a adăuga metadate sau de a schimba comportamentul declarației la care sunt atașați, fără a modifica direct structura codului subiacent. Acest mod extern, declarativ de a augmenta codul este incredibil de puternic.

Gândiți-vă la decoratori ca la adnotări sau etichete pe care le aplicați anumitor părți ale codului dumneavoastră. Aceste etichete pot fi apoi citite sau acționate de alte părți ale aplicației dumneavoastră sau de framework-uri, adesea la runtime, pentru a oferi funcționalități sau configurații suplimentare.

Sintaxa unui Decorator

Decoratorii sunt prefixați cu simbolul @, urmat de numele funcției decoratorului. Aceștia sunt plasați imediat înaintea declarației pe care o decorează.

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

Activarea Decoratorilor în TypeScript

Înainte de a putea utiliza decoratori, trebuie să activați opțiunea de compilare experimentalDecorators în fișierul dumneavoastră tsconfig.json. În plus, pentru capabilități avansate de reflecție a metadatelor (adesea utilizate de framework-uri), veți avea nevoie și de emitDecoratorMetadata și de polyfill-ul 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}

De asemenea, trebuie să instalați reflect-metadata:

npm install reflect-metadata --save\n# or\nyarn add reflect-metadata

Și importați-l la începutul punctului de intrare al aplicației dumneavoastră (ex: main.ts sau app.ts):

import \"reflect-metadata\";\n// Your application code follows

Fabricile de Decoratori: Personalizare la Îndemână

Deși un decorator de bază este o funcție, adesea va trebui să transmiteți argumente unui decorator pentru a-i configura comportamentul. Acest lucru se realizează utilizând o fabrică de decoratori. O fabrică de decoratori este o funcție care returnează funcția decorator reală. Atunci când aplicați o fabrică de decoratori, o apelați cu argumentele sale, iar aceasta returnează apoi funcția decorator pe care TypeScript o aplică codului dumneavoastră.

Crearea unui Exemplu Simplu de Fabrică de Decoratori

Să creăm o fabrică pentru un decorator Logger care poate înregistra mesaje cu prefixe diferite.

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// Output:\n// [APP_INIT] Class ApplicationBootstrap has been defined.\n// Application is starting...

În acest exemplu, Logger("APP_INIT") este apelul fabricii de decoratori. Aceasta returnează funcția decorator reală care primește target: Function (constructorul clasei) ca argument. Acest lucru permite configurarea dinamică a comportamentului decoratorului.

Tipuri de Decoratori în TypeScript

TypeScript suportă cinci tipuri distincte de decoratori, fiecare aplicabil unui anumit tip de declarație. Semnătura funcției decoratorului variază în funcție de contextul în care este aplicată.

1. Decoratori de Clasă

Decoratorii de clasă sunt aplicați declarațiilor de clasă. Funcția decorator primește constructorul clasei ca singur argument. Un decorator de clasă poate observa, modifica sau chiar înlocui o definiție de clasă.

Semnătură:

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

Valoare Returnată:

Dacă decoratorul de clasă returnează o valoare, acesta va înlocui declarația de clasă cu funcția constructor furnizată. Aceasta este o caracteristică puternică, adesea utilizată pentru mixin-uri sau augmentarea clasei. Dacă nu este returnată nicio valoare, se utilizează clasa originală.

Cazuri de Utilizare:

Exemplu de Decorator de Clasă: Injectarea unui Serviciu

Imaginați-vă un scenariu simplu de injecție de dependențe în care doriți să marcați o clasă ca "injectabilă" și, opțional, să-i oferiți un nume într-un container.

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    // Optionally, you could return a new class here to augment behavior\n    return class extends constructor {\n      createdAt = new Date();\n      // Additional properties or methods for all injected services\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); // If the returned class is used\n}

Acest exemplu demonstrează cum un decorator de clasă poate înregistra o clasă și chiar îi poate modifica constructorul. Decoratorul Injectable face clasa descoperibilă de către un sistem teoretic de injecție de dependențe.

2. Decoratori de Metodă

Decoratorii de metodă sunt aplicați declarațiilor de metodă. Aceștia primesc trei argumente: obiectul țintă (pentru membrii statici, funcția constructor; pentru membrii instanței, prototipul clasei), numele metodei și descriptorul de proprietate al metodei.

Semnătură:

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

Valoare Returnată:

Un decorator de metodă poate returna un nou PropertyDescriptor. Dacă o face, acest descriptor va fi utilizat pentru a defini metoda. Acest lucru vă permite să modificați sau să înlocuiți implementarea metodei originale, făcând-o incredibil de puternică pentru AOP.

Cazuri de Utilizare:

Exemplu de Decorator de Metodă: Monitorizarea Performanței

Să creăm un decorator MeasurePerformance pentru a înregistra timpul de execuție al unei metode.

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    // Simulate a complex, time-consuming operation\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));

Decoratorul MeasurePerformance învelește metoda originală cu logică de temporizare, afișând durata execuției fără a aglomera logica de business din interiorul metodei. Acesta este un exemplu clasic de Programare Orientată pe Aspecte (AOP).

3. Decoratori de Accesor

Decoratorii de accesor sunt aplicați declarațiilor de accesor (get și set). Similar cu decoratorii de metodă, aceștia primesc obiectul țintă, numele accesorului și descriptorul de proprietate al acestuia.

Semnătură:

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

Valoare Returnată:

Un decorator de accesor poate returna un nou PropertyDescriptor, care va fi utilizat pentru a defini accesorul.

Cazuri de Utilizare:

Exemplu de Decorator de Accesor: Memorarea în Cache a Getter-ilor

Să creăm un decorator care memorează în cache rezultatul unei computații costisitoare a unui getter.

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  // Simulates an expensive computation\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);

Acest decorator asigură că computația getter-ului expensiveSummary rulează o singură dată, apelurile ulterioare returnând valoarea memorată în cache. Acest model este foarte util pentru optimizarea performanței acolo unde accesul la proprietate implică o computație intensivă sau apeluri externe.

4. Decoratori de Proprietate

Decoratorii de proprietate sunt aplicați declarațiilor de proprietate. Aceștia primesc două argumente: obiectul țintă (pentru membrii statici, funcția constructor; pentru membrii instanței, prototipul clasei) și numele proprietății.

Semnătură:

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

Valoare Returnată:

Decoratorii de proprietate nu pot returna nicio valoare. Utilizarea lor principală este de a înregistra metadate despre proprietate. Aceștia nu pot modifica direct valoarea proprietății sau descriptorul acesteia la momentul decorării, deoarece descriptorul pentru o proprietate nu este încă complet definit atunci când sunt rulați decoratorii de proprietate.

Cazuri de Utilizare:

Exemplu de Decorator de Proprietate: Validarea Câmpurilor Obligatorii

Să creăm un decorator pentru a marca o proprietate ca "obligatorie" și apoi să o validăm la runtime.

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.\"]

Decoratorul Required înregistrează pur și simplu regula de validare cu o hartă centrală validationRules. O funcție validate separată utilizează apoi aceste metadate pentru a verifica instanța la runtime. Acest model separă logica de validare de definiția datelor, făcând-o reutilizabilă și curată.

5. Decoratori de Parametru

Decoratorii de parametru sunt aplicați parametrilor dintr-un constructor de clasă sau dintr-o metodă. Aceștia primesc trei argumente: obiectul țintă (pentru membrii statici, funcția constructor; pentru membrii instanței, prototipul clasei), numele metodei (sau undefined pentru parametrii constructorului) și indexul ordinal al parametrului în lista de parametri a funcției.

Semnătură:

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

Valoare Returnată:

Decoratorii de parametru nu pot returna nicio valoare. La fel ca decoratorii de proprietate, rolul lor principal este de a adăuga metadate despre parametru.

Cazuri de Utilizare:

Exemplu de Decorator de Parametru: Injectarea Datelor din Cerere

Să simulăm cum un framework web ar putea utiliza decoratori de parametru pentru a injecta date specifice într-un parametru de metodă, cum ar fi un ID de utilizator dintr-o cerere.

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// A hypothetical framework function to invoke a method with resolved parameters\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// Simulate an incoming request\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\" });

Acest exemplu demonstrează cum decoratorii de parametru pot colecta informații despre parametrii de metodă necesari. Un framework poate utiliza apoi aceste metadate colectate pentru a rezolva și injecta automat valorile corespunzătoare atunci când metoda este apelată, simplificând semnificativ logica controlerului sau a serviciului.

Compoziția Decoratorilor și Ordinea de Execuție

Decoratorii pot fi aplicați în diverse combinații, iar înțelegerea ordinii lor de execuție este crucială pentru a prezice comportamentul și a evita probleme neașteptate.

Decoratori Multipli pe o Singură Țintă

Atunci când mai mulți decoratori sunt aplicați unei singure declarații (ex: o clasă, metodă sau proprietate), aceștia se execută într-o ordine specifică: de jos în sus, sau de la dreapta la stânga, pentru evaluarea lor. Cu toate acestea, rezultatele lor sunt aplicate în ordine inversă.

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

Aici, DecoratorB va fi evaluat primul, apoi DecoratorA. Dacă modifică clasa (ex: prin returnarea unui nou constructor), modificarea de la DecoratorA va înveli sau se va aplica peste modificarea de la DecoratorB.

Exemplu: Înlănțuirea Decoratorilor de Metodă

Considerați doi decoratori de metodă: LogCall și 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\"]; // Simulate fetching current user roles\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 // Order changed here\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  // Simulate a non-admin user trying to access fetchPublicData which requires 'user' role\n  const mockUserRoles = [\"guest\"]; // This will fail auth\n  // To make this dynamic, you'd need a DI system or static context for current user roles.\n  // For simplicity, we assume the Authorization decorator has access to current user context.\n  // Let's adjust Authorization decorator to always assume 'admin' for demo purposes, \n  // so the first call succeeds and second fails to show different paths.\n  \n  // Re-run with user role for fetchPublicData to succeed.\n  // Imagine currentUserRoles in Authorization becomes: ['user']\n  // For this example, let's keep it simple and show the order effect.\n  service.fetchPublicData(\"search term\"); // This will execute Auth -> Log\n} catch (error: any) {\n  console.error(error.message);\n}\n\n/* Expected output for 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/* Expected output for fetchPublicData (if user has 'user' role):\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*/

Observați ordinea: pentru deleteSensitiveData, Authorization (jos) rulează prima, apoi LogCall (sus) se înfășoară în jurul ei. Logica internă a Authorization se execută prima. Pentru fetchPublicData, LogCall (jos) rulează prima, apoi Authorization (sus) se înfășoară în jurul ei. Acest lucru înseamnă că aspectul LogCall va fi în afara aspectului Authorization. Această diferență este critică pentru aspectele transversale precum logarea sau gestionarea erorilor, unde ordinea de execuție poate influența semnificativ comportamentul.

Ordinea de Execuție pentru Ținte Diferite

Atunci când o clasă, membrii săi și parametrii au toți decoratori, ordinea de execuție este bine definită:

  1. Decoratorii de Parametru sunt aplicați primii, pentru fiecare parametru, începând de la ultimul parametru către primul.
  2. Apoi, Decoratorii de Metodă, Accesor sau Proprietate sunt aplicați pentru fiecare membru.
  3. În cele din urmă, Decoratorii de Clasă sunt aplicați clasei în sine.

În cadrul fiecărei categorii, mai mulți decoratori pe aceeași țintă sunt aplicați de jos în sus (sau de la dreapta la stânga).

Exemplu: Ordine Completă de Execuție

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; // Return descriptor for method/accessor, undefined for others\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// Call method to trigger method decorator\nnew MyDecoratedClass().myMethod(\"hello\", 123);\n\n/* Predicted Output Order (approximate, depending on specific TypeScript version and compilation):\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*/

Momentul exact de înregistrare în consolă poate varia ușor în funcție de momentul în care este invocat un constructor sau o metodă, dar ordinea în care funcțiile decoratorului în sine sunt executate (și, prin urmare, efectele lor secundare sau valorile returnate sunt aplicate) urmează regulile de mai sus.

Aplicații Practice și Modele de Design cu Decoratori

Decoratorii, în special în combinație cu polyfill-ul reflect-metadata, deschid un nou domeniu al programării bazate pe metadate. Acest lucru permite modele de design puternice care abstractizează codul boilerplate și aspectele transversale.

1. Iniecția de Dependențe (DI)

Una dintre cele mai proeminente utilizări ale decoratorilor este în framework-urile de Iniecție de Dependențe (cum ar fi @Injectable(), @Component() etc. din Angular, sau utilizarea extensivă a DI de către NestJS). Decoratorii vă permit să declarați dependențe direct pe constructori sau proprietăți, permițând framework-ului să instanțieze și să furnizeze automat serviciile corecte.

Exemplu: Iniecție Simplificată de Serviciu

import \"reflect-metadata\"; // Essential for 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\n    // Get constructor parameters' types (requires 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      // Use explicit @Inject token if provided, otherwise infer type\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// Define services\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; // Example of injecting via property using a custom decorator or framework feature\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// Resolve the main service\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();

Acest exemplu elaborat demonstrează cum decoratorii @Injectable și @Inject, combinați cu reflect-metadata, permit unui Container personalizat să rezolve și să furnizeze automat dependențe. Metadatele design:paramtypes emise automat de TypeScript (când emitDecoratorMetadata este adevărat) sunt cruciale aici.

2. Programare Orientată pe Aspecte (AOP)

AOP se concentrează pe modularizarea aspectelor transversale (ex: logare, securitate, tranzacții) care traversează mai multe clase și module. Decoratorii se potrivesc excelent pentru implementarea conceptelor AOP în TypeScript.

Exemplu: Logare cu Decorator de Metodă

Revizitând decoratorul LogCall, acesta este un exemplu perfect de AOP. Adaugă comportament de logare oricărei metode fără a modifica codul original al metodei. Aceasta separă "ce trebuie făcut" (logica de business) de "cum trebuie făcut" (logare, monitorizarea performanței etc.).

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}

Această abordare menține clasa PaymentProcessor concentrată pur pe logica de plată, în timp ce decoratorul LogMethod gestionează aspectul transversal al logării.

3. Validare și Transformare

Decoratorii sunt incredibil de utili pentru definirea regulilor de validare direct pe proprietăți sau pentru transformarea datelor în timpul serializării/deserializării.

Exemplu: Validarea Datelor cu Decoratori de Proprietate

Exemplul anterior cu @Required a demonstrat deja acest lucru. Iată un alt exemplu cu o validare a unui interval numeric.

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.\"]

Această configurație vă permite să definiți declarativ regulile de validare pe proprietățile modelului dumneavoastră, făcând ca modelele dumneavoastră de date să se auto-descrie în ceea ce privește constrângerile lor.

Cele Mai Bune Practici și Considerații

Deși decoratorii sunt puternici, aceștia ar trebui utilizați cu discernământ. Utilizarea lor greșită poate duce la un cod mai dificil de depanat sau de înțeles.

Când să Folosiți Decoratori (și Când Nu)

Implicații asupra Performanței

Decoratorii se execută la momentul compilării (sau la momentul definirii în runtime-ul JavaScript, dacă sunt transpilați). Transformarea sau colectarea metadatelor are loc atunci când clasa/metoda este definită, nu la fiecare apel. Prin urmare, impactul asupra performanței runtime al *aplicării* decoratorilor este minim. Cu toate acestea, *logica din interiorul* decoratorilor dumneavoastră poate avea un impact asupra performanței, mai ales dacă aceștia efectuează operații costisitoare la fiecare apel de metodă (ex: calcule complexe în cadrul unui decorator de metodă).

Mentenabilitate și Lizibilitate

Decoratorii, atunci când sunt utilizați corect, pot îmbunătăți semnificativ lizibilitatea, mutând codul boilerplate în afara logicii principale. Cu toate acestea, dacă efectuează transformări complexe, ascunse, depanarea poate deveni o provocare. Asigurați-vă că decoratorii dumneavoastră sunt bine documentați și că comportamentul lor este previzibil.

Statusul Experimental și Viitorul Decoratorilor

Este important de reiterat că decoratorii TypeScript se bazează pe o propunere TC39 de Etapa 3. Aceasta înseamnă că specificația este în mare parte stabilă, dar ar putea suferi în continuare modificări minore înainte de a deveni parte a standardului oficial ECMAScript. Framework-uri precum Angular i-au adoptat, mizând pe standardizarea lor eventuală. Acest lucru implică un anumit nivel de risc, deși având în vedere adoptarea lor pe scară largă, modificările majore care ar rupe compatibilitatea sunt improbabile.

Propunerea TC39 a evoluat. Implementarea curentă a TypeScript se bazează pe o versiune mai veche a propunerii. Există o distincție între "Decoratori Vechi" și "Decoratori Standard". Când standardul oficial va fi publicat, TypeScript își va actualiza probabil implementarea. Pentru majoritatea dezvoltatorilor care utilizează framework-uri, această tranziție va fi gestionată de framework-ul însuși. Pentru autorii de biblioteci, înțelegerea diferențelor subtile dintre decoratorii vechi și cei standard viitori ar putea deveni necesară.

Opțiunea de Compilare emitDecoratorMetadata

Această opțiune, când este setată la true în tsconfig.json, instruiește compilatorul TypeScript să emită anumite metadate de tip de design-time în JavaScript-ul compilat. Aceste metadate includ tipul parametrilor constructorului (design:paramtypes), tipul de returnare al metodelor (design:returntype) și tipul proprietăților (design:type).

Aceste metadate emise nu fac parte din runtime-ul standard JavaScript. Ele sunt de obicei consumate de polyfill-ul reflect-metadata, care le face apoi accesibile prin funcțiile Reflect.getMetadata(). Acest lucru este absolut critic pentru modele avansate precum Iniecția de Dependențe, unde un container trebuie să cunoască tipurile de dependențe pe care le necesită o clasă fără o configurare explicită.

Modele Avansate cu Decoratori

Decoratorii pot fi combinați și extinși pentru a construi modele și mai sofisticate.

1. Decorarea Decoratorilor (Decoratori de Ordin Superior)

Puteți crea decoratori care modifică sau compun alți decoratori. Acest lucru este mai puțin obișnuit, dar demonstrează natura funcțională a decoratorilor.

// A decorator that ensures a method is logged and also requires admin roles\nfunction AdminAndLoggedMethod() {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    // Apply Authorization first (inner)\n    Authorization([\"admin\"])(target, propertyKey, descriptor);\n    // Then apply LogCall (outer)\n    LogCall(target, propertyKey, descriptor);\n\n    return descriptor; // Return the modified 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/* Expected Output (assuming admin role):\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*/

Aici, AdminAndLoggedMethod este o fabrică care returnează un decorator, iar în interiorul acelui decorator, aplică alți doi decoratori. Acest model poate încapsula compoziții complexe de decoratori.

2. Utilizarea Decoratorilor pentru Mixin-uri

Deși TypeScript oferă alte modalități de a implementa mixin-uri, decoratorii pot fi utilizați pentru a injecta capabilități în clase într-un mod declarativ.

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  // These properties/methods are injected by the decorator\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}`);

Acest decorator @ApplyMixins copiază dinamic metode și proprietăți de la constructorii de bază la prototipul clasei derivate, "mixând" efectiv funcționalități.

Concluzie: Împuternicirea Dezvoltării Moderne cu TypeScript

Decoratorii TypeScript sunt o caracteristică puternică și expresivă care permite o nouă paradigmă de programare bazată pe metadate și orientată pe aspecte. Aceștia permit dezvoltatorilor să îmbunătățească, să modifice și să adauge comportamente declarative claselor, metodelor, proprietăților, accesorilor și parametrilor fără a le altera logica de bază. Această separare a preocupărilor duce la un cod mai curat, mai ușor de întreținut și foarte reutilizabil.

De la simplificarea injecției de dependențe și implementarea de sisteme robuste de validare până la adăugarea de aspecte transversale precum logarea și monitorizarea performanței, decoratorii oferă o soluție elegantă la multe provocări comune de dezvoltare. Deși statusul lor experimental impune prudență, adoptarea lor pe scară largă în framework-urile majore semnifică valoarea lor practică și relevanța viitoare.

Prin stăpânirea decoratorilor TypeScript, câștigați un instrument semnificativ în arsenalul dumneavoastră, permițându-vă să construiți aplicații mai robuste, scalabile și inteligente. Adoptați-i responsabil, înțelegeți-le mecanismele și deblocați un nou nivel de putere declarativă în proiectele dumneavoastră TypeScript.

Decoratorii TypeScript: Stăpânirea Modelelor de Programare cu Metadate pentru Aplicații Robuste | MLOG