Slovenčina

Objavte silu TypeScript dekorátorov pre metaprogramovanie, aspektovo orientované programovanie a vylepšovanie kódu deklaratívnymi vzormi. Komplexný sprievodca pre globálnych vývojárov.

TypeScript Decorátory: Zvládnutie vzorov metaprogramovania pre robustné aplikácie

V rozsiahlom svete moderného softvérového vývoja je udržiavanie čistých, škálovateľných a spravovateľných kódových základní kľúčové. TypeScript so svojím výkonným typovým systémom a pokročilými funkciami poskytuje vývojárom nástroje na dosiahnutie tohto cieľa. Medzi jeho najzaujímavejšie a najtransformatívnejšie funkcie patria dekorátory. Hoci v čase písania tohto článku sú stále experimentálnou funkciou (návrh v štádiu 3 pre ECMAScript), dekorátory sa vo veľkej miere používajú v frameworkoch ako Angular a TypeORM a zásadne menia náš prístup k návrhovým vzorom, metaprogramovaniu a aspektovo orientovanému programovaniu (AOP).

Tento komplexný sprievodca sa ponorí hlboko do TypeScript dekorátorov, preskúma ich mechanizmy, rôzne typy, praktické aplikácie a osvedčené postupy. Či už vytvárate rozsiahle podnikové aplikácie, mikroslužby alebo klientske webové rozhrania, pochopenie dekorátorov vám umožní písať deklaratívnejší, udržiavateľnejší a výkonnejší kód v TypeScript.

Pochopenie základného konceptu: Čo je dekorátor?

V podstate je dekorátor špeciálny druh deklarácie, ktorá môže byť pripojená k deklarácii triedy, metódy, prístupovej metódy (accessor), vlastnosti alebo parametra. Dekorátory sú funkcie, ktoré vracajú novú hodnotu (alebo modifikujú existujúcu) pre cieľ, ktorý dekorujú. Ich hlavným účelom je pridať metadáta alebo zmeniť správanie deklarácie, ku ktorej sú pripojené, bez priamej úpravy základnej štruktúry kódu. Tento externý, deklaratívny spôsob rozširovania kódu je neuveriteľne silný.

Predstavte si dekorátory ako anotácie alebo štítky, ktoré aplikujete na časti vášho kódu. Tieto štítky môžu byť následne čítané alebo spracované inými časťami vašej aplikácie alebo frameworkmi, často za behu, aby poskytli dodatočnú funkcionalitu alebo konfiguráciu.

Syntax dekorátora

Dekorátory majú predponu symbolu @, za ktorým nasleduje názov funkcie dekorátora. Umiestňujú sa bezprostredne pred deklaráciu, ktorú dekorujú.

@MyDecorator
class MyClass {
  @AnotherDecorator
  myMethod() {
    // ...
  }
}

Povolenie dekorátorov v TypeScript

Predtým, ako môžete začať používať dekorátory, musíte povoliť možnosť kompilátora experimentalDecorators vo vašom súbore tsconfig.json. Navyše, pre pokročilé možnosti reflexie metadát (často používané frameworkmi), budete tiež potrebovať emitDecoratorMetadata a polyfill reflect-metadata.

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

Taktiež je potrebné nainštalovať reflect-metadata:

npm install reflect-metadata --save
# or
yarn add reflect-metadata

A importovať ho na samom začiatku vstupného bodu vašej aplikácie (napr. main.ts alebo app.ts):

import "reflect-metadata";
// Nasleduje kód vašej aplikácie

Tvorba dekorátorov (Decorator Factories): Prispôsobenie na dosah ruky

Zatiaľ čo základný dekorátor je funkcia, často budete potrebovať odovzdať argumenty dekorátoru na konfiguráciu jeho správania. To sa dosahuje použitím továrne na dekorátory (decorator factory). Továreň na dekorátory je funkcia, ktorá vracia samotnú funkciu dekorátora. Keď aplikujete továreň na dekorátory, zavoláte ju s jej argumentmi a ona následne vráti funkciu dekorátora, ktorú TypeScript aplikuje na váš kód.

Príklad vytvorenia jednoduchej továrne na dekorátory

Vytvorme továreň pre dekorátor Logger, ktorý dokáže zaznamenávať správy s rôznymi predponami.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Class ${target.name} has been defined.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Application is starting...");
  }
}

const app = new ApplicationBootstrap();
// Výstup:
// [APP_INIT] Class ApplicationBootstrap has been defined.
// Application is starting...

V tomto príklade je Logger("APP_INIT") volanie továrne na dekorátory. Vráti samotnú funkciu dekorátora, ktorá prijíma target: Function (konštruktor triedy) ako svoj argument. To umožňuje dynamickú konfiguráciu správania dekorátora.

Typy dekorátorov v TypeScript

TypeScript podporuje päť odlišných typov dekorátorov, z ktorých každý je použiteľný na špecifický druh deklarácie. Signatúra funkcie dekorátora sa líši v závislosti od kontextu, v ktorom je aplikovaná.

1. Triedne dekorátory

Triedne dekorátory sa aplikujú na deklarácie tried. Funkcia dekorátora dostane konštruktor triedy ako svoj jediný argument. Triedny dekorátor môže sledovať, modifikovať alebo dokonca nahradiť definíciu triedy.

Signatúra:

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

Návratová hodnota:

Ak triedny dekorátor vráti hodnotu, nahradí deklaráciu triedy poskytnutou funkciou konštruktora. Toto je silná funkcia, často používaná pre mixiny alebo rozširovanie tried. Ak sa žiadna hodnota nevráti, použije sa pôvodná trieda.

Prípady použitia:

Príklad triedneho dekorátora: Injektovanie služby

Predstavme si jednoduchý scenár injektovania závislostí, kde chcete označiť triedu ako "injectable" a voliteľne jej poskytnúť názov v kontajneri.

const InjectableServiceRegistry = new Map<string, Function>();

function Injectable(name?: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    const serviceName = name || constructor.name;
    InjectableServiceRegistry.set(serviceName, constructor);
    console.log(`Registered service: ${serviceName}`);

    // Voliteľne tu môžete vrátiť novú triedu na rozšírenie správania
    return class extends constructor {
      createdAt = new Date();
      // Dodatočné vlastnosti alebo metódy pre všetky injektované služby
    };
  };
}

@Injectable("UserService")
class UserDataService {
  getUsers() {
    return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
  }
}

@Injectable()
class ProductDataService {
  getProducts() {
    return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
  }
}

console.log("--- Services Registered ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Users:", userServiceInstance.getUsers());
  // console.log("User Service Created At:", userServiceInstance.createdAt); // Ak sa použije vrátená trieda
}

Tento príklad ukazuje, ako môže triedny dekorátor zaregistrovať triedu a dokonca modifikovať jej konštruktor. Dekorátor Injectable robí triedu zistiteľnou pre teoretický systém injektovania závislostí.

2. Metódové dekorátory

Metódové dekorátory sa aplikujú na deklarácie metód. Prijímajú tri argumenty: cieľový objekt (pre statické členy konštruktor funkcie; pre inštančné členy prototyp triedy), názov metódy a deskriptor vlastnosti metódy.

Signatúra:

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

Návratová hodnota:

Metódový dekorátor môže vrátiť nový PropertyDescriptor. Ak tak urobí, tento deskriptor sa použije na definovanie metódy. To vám umožňuje modifikovať alebo nahradiť implementáciu pôvodnej metódy, čo je neuveriteľne silné pre AOP.

Prípady použitia:

Príklad metódového dekorátora: Monitorovanie výkonu

Vytvorme dekorátor MeasurePerformance na logovanie času vykonávania metódy.

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    const start = process.hrtime.bigint();
    const result = originalMethod.apply(this, args);
    const end = process.hrtime.bigint();
    const duration = Number(end - start) / 1_000_000;
    console.log(`Method "${propertyKey}" executed in ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Simulácia zložitej, časovo náročnej operácie
    for (let i = 0; i < 1_000_000; i++) {
      Math.sin(i);
    }
    return data.map(n => n * 2);
  }

  @MeasurePerformance
  fetchRemoteData(id: string): Promise<string> {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(`Data for ID: ${id}`);
      }, 500);
    });
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));

Dekorátor MeasurePerformance obalí pôvodnú metódu logikou na meranie času, čím vypíše dĺžku vykonávania bez toho, aby zanášal obchodnú logiku v samotnej metóde. Toto je klasický príklad aspektovo orientovaného programovania (AOP).

3. Dekorátory prístupových metód (Accessor)

Dekorátory prístupových metód sa aplikujú na deklarácie prístupových metód (get a set). Podobne ako metódové dekorátory, prijímajú cieľový objekt, názov prístupovej metódy a jej deskriptor vlastnosti.

Signatúra:

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

Návratová hodnota:

Dekorátor prístupovej metódy môže vrátiť nový PropertyDescriptor, ktorý sa použije na definovanie prístupovej metódy.

Prípady použitia:

Príklad dekorátora prístupovej metódy: Kešovanie getterov

Vytvorme dekorátor, ktorý kešuje výsledok náročného výpočtu gettera.

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;
  const cacheKey = `_cached_${String(propertyKey)}`;

  if (originalGetter) {
    descriptor.get = function() {
      if (this[cacheKey] === undefined) {
        console.log(`[Cache Miss] Computing value for ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

  constructor(data: number[]) {
    this.data = data;
  }

  // Simuluje náročný výpočet
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Performing expensive summary calculation...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

const generator = new ReportGenerator([10, 20, 30, 40, 50]);

console.log("First access:", generator.expensiveSummary);
console.log("Second access:", generator.expensiveSummary);
console.log("Third access:", generator.expensiveSummary);

Tento dekorátor zaisťuje, že výpočet gettera expensiveSummary sa vykoná iba raz, nasledujúce volania vrátia hodnotu z keše. Tento vzor je veľmi užitočný na optimalizáciu výkonu, keď prístup k vlastnosti zahŕňa náročné výpočty alebo externé volania.

4. Dekorátory vlastností

Dekorátory vlastností sa aplikujú na deklarácie vlastností. Prijímajú dva argumenty: cieľový objekt (pre statické členy konštruktor funkcie; pre inštančné členy prototyp triedy) a názov vlastnosti.

Signatúra:

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

Návratová hodnota:

Dekorátory vlastností nemôžu vrátiť žiadnu hodnotu. Ich hlavným použitím je registrácia metadát o vlastnosti. Nemôžu priamo zmeniť hodnotu vlastnosti alebo jej deskriptor v čase dekorácie, pretože deskriptor pre vlastnosť ešte nie je plne definovaný, keď sa spúšťajú dekorátory vlastností.

Prípady použitia:

Príklad dekorátora vlastnosti: Validácia povinného poľa

Vytvorme dekorátor na označenie vlastnosti ako "povinná" a potom ju validujme za behu.

interface ValidationRule {
  property: string | symbol;
  validate: (value: any) => boolean;
  message: string;
}

const validationRules: Map<Function, ValidationRule[]> = new Map();

function Required(target: Object, propertyKey: string | symbol) {
  const rules = validationRules.get(target.constructor) || [];
  rules.push({
    property: propertyKey,
    validate: (value: any) => value !== null && value !== undefined && value !== "",
    message: `${String(propertyKey)} is required.`
  });
  validationRules.set(target.constructor, rules);
}

function validate(instance: any): string[] {
  const classRules = validationRules.get(instance.constructor) || [];
  const errors: string[] = [];

  for (const rule of classRules) {
    if (!rule.validate(instance[rule.property])) {
      errors.push(rule.message);
    }
  }
  return errors;
}

class UserProfile {
  @Required
  firstName: string;

  @Required
  lastName: string;

  age?: number;

  constructor(firstName: string, lastName: string, age?: number) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

const user1 = new UserProfile("John", "Doe", 30);
console.log("User 1 validation errors:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("User 2 validation errors:", validate(user2)); // ["firstName is required."]

const user3 = new UserProfile("Alice", "");
console.log("User 3 validation errors:", validate(user3)); // ["lastName is required."]

Dekorátor Required jednoducho zaregistruje validačné pravidlo v centrálnej mape validationRules. Samostatná funkcia validate potom použije tieto metadáta na kontrolu inštancie za behu. Tento vzor oddeľuje validačnú logiku od definície dát, čím ju robí znovupoužiteľnou a čistou.

5. Dekorátory parametrov

Dekorátory parametrov sa aplikujú na parametre v konštruktore triedy alebo v metóde. Prijímajú tri argumenty: cieľový objekt (pre statické členy konštruktor funkcie; pre inštančné členy prototyp triedy), názov metódy (alebo undefined pre parametre konštruktora) a poradový index parametra v zozname parametrov funkcie.

Signatúra:

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

Návratová hodnota:

Dekorátory parametrov nemôžu vrátiť žiadnu hodnotu. Podobne ako dekorátory vlastností, ich hlavnou úlohou je pridať metadáta o parametri.

Prípady použitia:

Príklad dekorátora parametra: Injektovanie dát z požiadavky

Simulujme, ako by webový framework mohol použiť dekorátory parametrov na injektovanie špecifických dát do parametra metódy, ako je napríklad ID používateľa z požiadavky.

interface ParameterMetadata {
  index: number;
  key: string | symbol;
  resolver: (request: any) => any;
}

const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();

function RequestParam(paramName: string) {
  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
    const targetKey = propertyKey || "constructor";
    let methodResolvers = parameterResolvers.get(target.constructor);
    if (!methodResolvers) {
      methodResolvers = new Map();
      parameterResolvers.set(target.constructor, methodResolvers);
    }
    const paramMetadata = methodResolvers.get(targetKey) || [];
    paramMetadata.push({
      index: parameterIndex,
      key: targetKey,
      resolver: (request: any) => request[paramName]
    });
    methodResolvers.set(targetKey, paramMetadata);
  };
}

// Hypotetická funkcia frameworku na vyvolanie metódy s vyriešenými parametrami
function executeWithParams(instance: any, methodName: string, request: any) {
  const classResolvers = parameterResolvers.get(instance.constructor);
  if (!classResolvers) {
    return (instance[methodName] as Function).apply(instance, []);
  }
  const methodParamMetadata = classResolvers.get(methodName);
  if (!methodParamMetadata) {
    return (instance[methodName] as Function).apply(instance, []);
  }

  const args: any[] = Array(methodParamMetadata.length);
  for (const meta of methodParamMetadata) {
    args[meta.index] = meta.resolver(request);
  }
  return (instance[methodName] as Function).apply(instance, args);
}

class UserController {
  getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
    console.log(`Fetching user with ID: ${userId}, Token: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Deleting user with ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// Simulácia prichádzajúcej požiadavky
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("
--- Executing getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("
--- Executing deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

Tento príklad ukazuje, ako môžu dekorátory parametrov zbierať informácie o požadovaných parametroch metódy. Framework môže potom použiť tieto zozbierané metadáta na automatické vyriešenie a injektovanie príslušných hodnôt pri volaní metódy, čo výrazne zjednodušuje logiku kontroléra alebo služby.

Skladanie dekorátorov a poradie vykonávania

Dekorátory sa dajú aplikovať v rôznych kombináciách a pochopenie ich poradia vykonávania je kľúčové pre predpovedanie správania a predchádzanie neočakávaným problémom.

Viacnásobné dekorátory na jednom cieli

Keď sa na jednu deklaráciu (napr. triedu, metódu alebo vlastnosť) aplikuje viacero dekorátorov, vykonávajú sa v špecifickom poradí: zdola nahor alebo sprava doľava, pokiaľ ide o ich vyhodnotenie. Ich výsledky sa však aplikujú v opačnom poradí.

@DecoratorA
@DecoratorB
class MyClass {
  // ...
}

Tu sa najprv vyhodnotí DecoratorB, potom DecoratorA. Ak modifikujú triedu (napr. vrátením nového konštruktora), modifikácia z DecoratorA obalí alebo sa aplikuje nad modifikáciu z DecoratorB.

Príklad: Reťazenie metódových dekorátorov

Zvážme dva metódové dekorátory: LogCall a Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Method ${String(propertyKey)} returned:`, result);
    return result;
  };
  return descriptor;
}

function Authorization(roles: string[]) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const currentUserRoles = ["admin"]; // Simulácia získania rolí aktuálneho používateľa
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(", ")}`);
        throw new Error("Unauthorized access");
      }
      console.log(`[AUTH] Access granted for ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Deleting sensitive data for ID: ${id}`);
    return `Data ID ${id} deleted.`;
  }

  @Authorization(["user"])
  @LogCall // Poradie sa tu zmenilo
  fetchPublicData(query: string) {
    console.log(`Fetching public data with query: ${query}`);
    return `Public data for query: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("
--- Calling deleteSensitiveData (Admin User) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("
--- Calling fetchPublicData (Non-Admin User) ---");
  // Simulácia ne-admin používateľa, ktorý sa snaží získať prístup k fetchPublicData, ktorá vyžaduje rolu 'user'
  const mockUserRoles = ["guest"]; // Toto zlyhá pri autentifikácii
  // Aby to bolo dynamické, potrebovali by ste DI systém alebo statický kontext pre roly aktuálneho používateľa.
  // Pre zjednodušenie predpokladáme, že dekorátor Authorization má prístup k kontextu aktuálneho používateľa.
  // Upravme dekorátor Authorization tak, aby vždy predpokladal 'admin' pre demo účely,
  // aby prvé volanie uspelo a druhé zlyhalo, aby sa ukázali rôzne cesty.
  
  // Spustite znova s rolou používateľa, aby fetchPublicData uspelo.
  // Predstavte si, že currentUserRoles v Authorization sa stane: ['user']
  // Pre tento príklad to nechajme jednoduché a ukážme efekt poradia.
  service.fetchPublicData("search term"); // Toto vykoná Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Očakávaný výstup pre deleteSensitiveData:
[AUTH] Access granted for deleteSensitiveData
[LOG] Calling deleteSensitiveData with args: [ 'record123' ]
Deleting sensitive data for ID: record123
[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.
*/

/* Očakávaný výstup pre fetchPublicData (ak má používateľ rolu 'user'):
[LOG] Calling fetchPublicData with args: [ 'search term' ]
[AUTH] Access granted for fetchPublicData
Fetching public data with query: search term
[LOG] Method fetchPublicData returned: Public data for query: search term
*/

Všimnite si poradie: pre deleteSensitiveData sa najprv spustí Authorization (spodný), potom ho obalí LogCall (vrchný). Vnútorná logika Authorization sa vykoná ako prvá. Pre fetchPublicData sa najprv spustí LogCall (spodný), potom ho obalí Authorization (vrchný). To znamená, že aspekt LogCall bude mimo aspektu Authorization. Tento rozdiel je kritický pre prierezové záležitosti (cross-cutting concerns) ako je logovanie alebo spracovanie chýb, kde poradie vykonávania môže výrazne ovplyvniť správanie.

Poradie vykonávania pre rôzne ciele

Keď majú trieda, jej členovia a parametre dekorátory, poradie vykonávania je presne definované:

  1. Dekorátory parametrov sa aplikujú ako prvé, pre každý parameter, od posledného parametra po prvý.
  2. Potom sa aplikujú dekorátory metód, prístupových metód alebo vlastností pre každého člena.
  3. Nakoniec sa aplikujú triedne dekorátory na samotnú triedu.

V rámci každej kategórie sa viacero dekorátorov na tom istom cieli aplikuje zdola nahor (alebo sprava doľava).

Príklad: Úplné poradie vykonávania

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);
      } else {
        console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);
      }
    } else {
      console.log(`Class Decorator: ${message} on ${target.name}`);
    }
    return descriptorOrIndex; // Vrátiť deskriptor pre metódu/prístupovú metódu, undefined pre ostatné
  };
}

@log("Class Level D")
@log("Class Level C")
class MyDecoratedClass {
  @log("Static Property A")
  static staticProp: string = "";

  @log("Instance Property B")
  instanceProp: number = 0;

  @log("Method D")
  @log("Method C")
  myMethod(
    @log("Parameter Z") paramZ: string,
    @log("Parameter Y") paramY: number
  ) {
    console.log("Method myMethod executed.");
  }

  @log("Getter/Setter F")
  get myAccessor() {
    return "";
  }

  set myAccessor(value: string) {
    //...
  }

  constructor() {
    console.log("Constructor executed.");
  }
}

new MyDecoratedClass();
// Zavolanie metódy na spustenie metódového dekorátora
new MyDecoratedClass().myMethod("hello", 123);

/* Predpokladané poradie výstupu (približné, v závislosti od konkrétnej verzie TypeScriptu a kompilácie):
Param Decorator: Parameter Y on parameter #1 of myMethod
Param Decorator: Parameter Z on parameter #0 of myMethod
Property Decorator: Static Property A on staticProp
Property Decorator: Instance Property B on instanceProp
Method/Accessor Decorator: Getter/Setter F on myAccessor
Method/Accessor Decorator: Method C on myMethod
Method/Accessor Decorator: Method D on myMethod
Class Decorator: Class Level C on MyDecoratedClass
Class Decorator: Class Level D on MyDecoratedClass
Constructor executed.
Method myMethod executed.
*/

Presné načasovanie výpisu do konzoly sa môže mierne líšiť v závislosti od toho, kedy je konštruktor alebo metóda zavolaná, ale poradie, v ktorom sa samotné funkcie dekorátorov vykonávajú (a teda aj ich vedľajšie účinky alebo vrátené hodnoty), sa riadi vyššie uvedenými pravidlami.

Praktické aplikácie a návrhové vzory s dekorátormi

Dekorátory, najmä v spojení s polyfillom reflect-metadata, otvárajú novú sféru programovania riadeného metadátami. To umožňuje silné návrhové vzory, ktoré abstrahujú opakujúci sa kód a prierezové záležitosti.

1. Injektovanie závislostí (DI)

Jedným z najvýznamnejších použití dekorátorov je v frameworkoch pre injektovanie závislostí (ako napríklad @Injectable(), @Component() atď. v Angulari, alebo rozsiahle použitie DI v NestJS). Dekorátory vám umožňujú deklarovať závislosti priamo na konštruktoroch alebo vlastnostiach, čo umožňuje frameworku automaticky inštancovať a poskytnúť správne služby.

Príklad: Zjednodušené injektovanie služby

import "reflect-metadata"; // Nevyhnutné pre emitDecoratorMetadata

const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");

function Injectable() {
  return function (target: Function) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
  };
}

function Inject(token: any) {
  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
    existingInjections[parameterIndex] = token;
    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
  };
}

class Container {
  private static instances = new Map<any, any>();

  static resolve<T>(target: { new (...args: any[]): T }): T {
    if (Container.instances.has(target)) {
      return Container.instances.get(target);
    }

    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
    if (!isInjectable) {
      throw new Error(`Class ${target.name} is not marked as @Injectable.`);
    }

    // Získať typy parametrov konštruktora (vyžaduje emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Použiť explicitný @Inject token, ak je poskytnutý, inak odvodiť typ
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// Definovať služby
@Injectable()
class DatabaseService {
  connect() {
    console.log("Connecting to database...");
    return "DB Connection";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Authenticating using ${this.db.connect()}`);
    return "User logged in";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Príklad injektovania cez vlastnosť pomocou vlastného dekorátora alebo funkcie frameworku

  constructor(@Inject(AuthService) authService: AuthService,
              @Inject(DatabaseService) dbService: DatabaseService) {
    this.authService = authService;
    this.dbService = dbService;
  }

  getUserProfile() {
    this.authService.login();
    this.dbService.connect();
    console.log("UserService: Fetching user profile...");
    return { id: 1, name: "Global User" };
  }
}

// Vyriešiť hlavnú službu
console.log("--- Resolving UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("
--- Resolving AuthService (should be cached) ---");
const authService = Container.resolve(AuthService);
authService.login();

Tento prepracovaný príklad ukazuje, ako dekorátory @Injectable a @Inject v kombinácii s reflect-metadata umožňujú vlastnému Container-u automaticky riešiť a poskytovať závislosti. Metadáta design:paramtypes, ktoré TypeScript automaticky emituje (keď je emitDecoratorMetadata nastavené na true), sú tu kľúčové.

2. Aspektovo orientované programovanie (AOP)

AOP sa zameriava na modularizáciu prierezových záležitostí (napr. logovanie, bezpečnosť, transakcie), ktoré sa prelínajú viacerými triedami a modulmi. Dekorátory sú vynikajúcim nástrojom na implementáciu konceptov AOP v TypeScript.

Príklad: Logovanie pomocou metódového dekorátora

Vráťme sa k dekorátoru LogCall, je to dokonalý príklad AOP. Pridáva logovacie správanie akejkoľvek metóde bez úpravy pôvodného kódu metódy. Tým sa oddeľuje "čo robiť" (obchodná logika) od "ako to robiť" (logovanie, monitorovanie výkonu atď.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Payment amount must be positive.");
    }
    console.log(`Processing payment of ${amount} ${currency}...`);
    return `Payment of ${amount} ${currency} processed successfully.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Refunding payment for transaction ID: ${transactionId}...`);
    return `Refund initiated for ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.error("Caught error:", error.message);
}

Tento prístup udržuje triedu PaymentProcessor zameranú čisto na platobnú logiku, zatiaľ čo dekorátor LogMethod sa stará o prierezovú záležitosť logovania.

3. Validácia a transformácia

Dekorátory sú neuveriteľne užitočné na definovanie validačných pravidiel priamo na vlastnostiach alebo na transformáciu dát počas serializácie/deserializácie.

Príklad: Validácia dát pomocou dekorátorov vlastností

Príklad @Required to už demonštroval. Tu je ďalší príklad s validáciou číselného rozsahu.

interface FieldValidationRule {
  property: string | symbol;
  validator: (value: any) => boolean;
  message: string;
}

const fieldValidationRules = new Map<Function, FieldValidationRule[]>();

function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
  const rules = fieldValidationRules.get(target.constructor) || [];
  rules.push({ property: propertyKey, validator, message });
  fieldValidationRules.set(target.constructor, rules);
}

function IsPositive(target: Object, propertyKey: string | symbol) {
  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} must be a positive number.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);
  };
}

class Product {
  @MaxLength(50)
  name: string;

  @IsPositive
  price: number;

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  static validate(instance: any): string[] {
    const errors: string[] = [];
    const rules = fieldValidationRules.get(instance.constructor) || [];
    for (const rule of rules) {
      if (!rule.validator(instance[rule.property])) {
        errors.push(rule.message);
      }
    }
    return errors;
  }
}

const product1 = new Product("Laptop", 1200);
console.log("Product 1 errors:", Product.validate(product1)); // []

const product2 = new Product("Very long product name that exceeds fifty characters limit for testing purpose", 50);
console.log("Product 2 errors:", Product.validate(product2)); // ["name must be at most 50 characters long."]

const product3 = new Product("Book", -10);
console.log("Product 3 errors:", Product.validate(product3)); // ["price must be a positive number."]

Toto nastavenie vám umožňuje deklaratívne definovať validačné pravidlá na vlastnostiach vášho modelu, čím sa vaše dátové modely stávajú samopopisnými z hľadiska ich obmedzení.

Osvedčené postupy a úvahy

Hoci sú dekorátory silné, mali by sa používať uvážlivo. Ich nesprávne použitie môže viesť ku kódu, ktorý je ťažšie ladiť alebo pochopiť.

Kedy používať dekorátory (a kedy nie)

Dôsledky na výkon

Dekorátory sa vykonávajú v čase kompilácie (alebo v čase definície v JavaScript runtime, ak sú transpiled). Transformácia alebo zber metadát sa deje, keď je trieda/metóda definovaná, nie pri každom volaní. Preto je dopad na výkon za behu pri *aplikovaní* dekorátorov minimálny. Avšak, *logika vnútri* vašich dekorátorov môže mať dopad na výkon, najmä ak vykonávajú náročné operácie pri každom volaní metódy (napr. zložité výpočty v rámci metódového dekorátora).

Udržiavateľnosť a čitateľnosť

Dekorátory, ak sa používajú správne, môžu výrazne zlepšiť čitateľnosť presunutím opakujúceho sa kódu mimo hlavnej logiky. Avšak, ak vykonávajú zložité, skryté transformácie, ladenie sa môže stať náročným. Uistite sa, že vaše dekorátory sú dobre zdokumentované a ich správanie je predvídateľné.

Experimentálny stav a budúcnosť dekorátorov

Je dôležité zopakovať, že TypeScript dekorátory sú založené na návrhu TC39 v štádiu 3. To znamená, že špecifikácia je z veľkej časti stabilná, ale stále by mohla prejsť menšími zmenami predtým, ako sa stane súčasťou oficiálneho štandardu ECMAScript. Frameworky ako Angular ich prijali, staviac na ich konečnú štandardizáciu. To so sebou prináša určitú mieru rizika, hoci vzhľadom na ich rozšírené prijatie sú významné zmeny, ktoré by porušili kompatibilitu, nepravdepodobné.

Návrh TC39 sa vyvíjal. Súčasná implementácia v TypeScript je založená na staršej verzii návrhu. Existuje rozdiel medzi "Legacy Decorators" a "Standard Decorators". Keď bude oficiálny štandard prijatý, TypeScript pravdepodobne aktualizuje svoju implementáciu. Pre väčšinu vývojárov používajúcich frameworky bude tento prechod spravovaný samotným frameworkom. Pre autorov knižníc môže byť potrebné pochopiť jemné rozdiely medzi staršími a budúcimi štandardnými dekorátormi.

Možnosť kompilátora emitDecoratorMetadata

Táto možnosť, keď je nastavená na true v tsconfig.json, inštruuje kompilátor TypeScript, aby emitoval určité metadáta typov z návrhového času do kompilovaného JavaScriptu. Tieto metadáta zahŕňajú typ parametrov konštruktora (design:paramtypes), návratový typ metód (design:returntype) a typ vlastností (design:type).

Tieto emitované metadáta nie sú súčasťou štandardného JavaScript runtime. Typicky ich spotrebúva polyfill reflect-metadata, ktorý ich potom sprístupňuje prostredníctvom funkcií Reflect.getMetadata(). Toto je absolútne kľúčové pre pokročilé vzory ako injektovanie závislostí, kde kontajner potrebuje poznať typy závislostí, ktoré trieda vyžaduje, bez explicitnej konfigurácie.

Pokročilé vzory s dekorátormi

Dekorátory sa dajú kombinovať a rozširovať na vytváranie ešte sofistikovanejších vzorov.

1. Dekorovania dekorátorov (Higher-Order Decorators)

Môžete vytvárať dekorátory, ktoré modifikujú alebo skladajú iné dekorátory. Je to menej bežné, ale demonštruje funkcionálnu povahu dekorátorov.

// Dekorátor, ktorý zaisťuje, že metóda je logovaná a zároveň vyžaduje admin roly
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Aplikovať Authorization ako prvý (vnútorný)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Potom aplikovať LogCall (vonkajší)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Vrátiť modifikovaný deskriptor
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Deleting user account: ${userId}`);
    return `User ${userId} deleted.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Očakávaný výstup (za predpokladu admin roly):
[AUTH] Access granted for deleteUserAccount
[LOG] Calling deleteUserAccount with args: [ 'user007' ]
Deleting user account: user007
[LOG] Method deleteUserAccount returned: User user007 deleted.
*/

Tu je AdminAndLoggedMethod továreň, ktorá vracia dekorátor, a vnútri tohto dekorátora aplikuje dva ďalšie dekorátory. Tento vzor môže zapuzdriť zložité kompozície dekorátorov.

2. Použitie dekorátorov pre Mixiny

Hoci TypeScript ponúka iné spôsoby implementácie mixinov, dekorátory sa dajú použiť na injektovanie schopností do tried deklaratívnym spôsobom.

function ApplyMixins(constructors: Function[]) {
  return function (derivedConstructor: Function) {
    constructors.forEach(baseConstructor => {
      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
        Object.defineProperty(
          derivedConstructor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
        );
      });
    });
  };
}

class Disposable {
  isDisposed: boolean = false;
  dispose() {
    this.isDisposed = true;
    console.log("Object disposed.");
  }
}

class Loggable {
  log(message: string) {
    console.log(`[Loggable] ${message}`);
  }
}

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Tieto vlastnosti/metódy sú injektované dekorátorom
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Resource ${this.name} created.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Resource ${this.name} cleaned up.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Is disposed: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Is disposed: ${resource.isDisposed}`);

Tento dekorátor @ApplyMixins dynamicky kopíruje metódy a vlastnosti z bázových konštruktorov do prototypu odvodenej triedy, čím efektívne "primiešava" funkcionality.

Záver: Posilnenie moderného vývoja v TypeScript

TypeScript dekorátory sú silná a expresívna funkcia, ktorá umožňuje novú paradigmu programovania riadeného metadátami a aspektovo orientovaného programovania. Umožňujú vývojárom vylepšovať, modifikovať a pridávať deklaratívne správanie do tried, metód, vlastností, prístupových metód a parametrov bez zmeny ich základnej logiky. Toto oddelenie záležitostí vedie k čistejšiemu, udržiavateľnejšiemu a vysoko znovupoužiteľnému kódu.

Od zjednodušenia injektovania závislostí a implementácie robustných validačných systémov až po pridávanie prierezových záležitostí ako je logovanie a monitorovanie výkonu, dekorátory poskytujú elegantné riešenie mnohých bežných vývojárskych výziev. Hoci ich experimentálny stav si vyžaduje ostražitosť, ich rozsiahle prijatie v hlavných frameworkoch signalizuje ich praktickú hodnotu a budúcu relevanciu.

Zvládnutím TypeScript dekorátorov získate významný nástroj do svojho arzenálu, ktorý vám umožní vytvárať robustnejšie, škálovateľnejšie a inteligentnejšie aplikácie. Prijmite ich zodpovedne, pochopte ich mechanizmy a odomknite novú úroveň deklaratívnej sily vo vašich projektoch v TypeScript.