Čeština

Prozkoumejte sílu TypeScript dekorátorů pro programování s metadaty, aspektově orientované programování a vylepšení kódu pomocí deklarativních vzorů. Komplexní průvodce pro vývojáře.

TypeScript dekorátory: Zvládnutí programovacích vzorů s metadaty pro robustní aplikace

V rozsáhlém světě moderního vývoje softwaru je klíčové udržovat čisté, škálovatelné a spravovatelné kódové báze. TypeScript se svým výkonným typovým systémem a pokročilými funkcemi poskytuje vývojářům nástroje k dosažení tohoto cíle. Mezi jeho nejzajímavější a nejtransformativnější funkce patří dekorátory. Ačkoli jsou v době psaní tohoto textu stále experimentální funkcí (návrh ve fázi 3 pro ECMAScript), dekorátory jsou široce používány ve frameworcích jako Angular a TypeORM a zásadně mění způsob, jakým přistupujeme k návrhovým vzorům, programování s metadaty a aspektově orientovanému programování (AOP).

Tento komplexní průvodce se ponoří hluboko do TypeScript dekorátorů, prozkoumá jejich mechaniku, různé typy, praktické aplikace a osvědčené postupy. Ať už vytváříte rozsáhlé podnikové aplikace, mikroslužby nebo webová rozhraní na straně klienta, pochopení dekorátorů vám umožní psát deklarativnější, udržovatelnější a výkonnější kód v TypeScriptu.

Pochopení základního konceptu: Co je dekorátor?

V jádru je dekorátor speciální druh deklarace, který lze připojit k deklaraci třídy, metodě, přístupové metodě, vlastnosti nebo parametru. Dekorátory jsou funkce, které vracejí novou hodnotu (nebo upravují existující) pro cíl, který dekorují. Jejich primárním účelem je přidávat metadata nebo měnit chování deklarace, ke které jsou připojeny, aniž by přímo modifikovaly základní strukturu kódu. Tento externí, deklarativní způsob rozšiřování kódu je neuvěřitelně mocný.

Představte si dekorátory jako anotace nebo štítky, které aplikujete na části svého kódu. Tyto štítky pak mohou být čteny nebo na ně mohou reagovat jiné části vaší aplikace nebo frameworky, často za běhu, aby poskytly další funkcionalitu nebo konfiguraci.

Syntaxe dekorátoru

Dekorátory jsou označeny prefixem @, po kterém následuje název funkce dekorátoru. Umisťují se bezprostředně před deklaraci, kterou dekorují.

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

Povolení dekorátorů v TypeScriptu

Než budete moci používat dekorátory, musíte povolit volbu kompilátoru experimentalDecorators ve vašem souboru tsconfig.json. Kromě toho pro pokročilé možnosti reflexe metadat (často používané frameworky) budete také potřebovat 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
  }
}

Musíte také nainstalovat reflect-metadata:

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

A importovat ho úplně na začátek vstupního bodu vaší aplikace (např. main.ts nebo app.ts):

import "reflect-metadata";
// Následuje kód vaší aplikace

Továrny na dekorátory: Přizpůsobení na dosah ruky

Zatímco základní dekorátor je funkce, často budete potřebovat předat argumenty dekorátoru pro konfiguraci jeho chování. Toho se dosahuje pomocí továrny na dekorátory. Továrna na dekorátory je funkce, která vrací samotnou funkci dekorátoru. Když aplikujete továrnu na dekorátory, zavoláte ji s jejími argumenty a ona pak vrátí funkci dekorátoru, kterou TypeScript aplikuje na váš kód.

Příklad vytvoření jednoduché továrny na dekorátory

Vytvořme továrnu pro dekorátor Logger, který může logovat zprávy s různými prefixy.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Třída ${target.name} byla definována.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Aplikace se spouští...");
  }
}

const app = new ApplicationBootstrap();
// Výstup:
// [APP_INIT] Třída ApplicationBootstrap byla definována.
// Aplikace se spouští...

V tomto příkladu je Logger("APP_INIT") volání továrny na dekorátory. Vrací skutečnou funkci dekorátoru, která jako argument přebírá target: Function (konstruktor třídy). To umožňuje dynamickou konfiguraci chování dekorátoru.

Typy dekorátorů v TypeScriptu

TypeScript podporuje pět různých typů dekorátorů, z nichž každý je aplikovatelný na specifický druh deklarace. Signatura funkce dekorátoru se liší v závislosti na kontextu, ve kterém je aplikována.

1. Dekorátory tříd

Dekorátory tříd se aplikují na deklarace tříd. Funkce dekorátoru obdrží jako jediný argument konstruktor třídy. Dekorátor třídy může pozorovat, modifikovat nebo dokonce nahradit definici třídy.

Signatura:

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

Návratová hodnota:

Pokud dekorátor třídy vrátí hodnotu, nahradí deklaraci třídy poskytnutou konstruktorovou funkcí. Toto je silná funkce, často používaná pro mixiny nebo rozšíření tříd. Pokud není vrácena žádná hodnota, použije se původní třída.

Případy použití:

Příklad dekorátoru třídy: Injektování služby

Představte si jednoduchý scénář injektáže závislostí, kde chcete označit třídu jako „injektovatelnou“ a volitelně pro ni poskytnout název v kontejneru.

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(`Zaregistrována služba: ${serviceName}`);

    // Volitelně zde můžete vrátit novou třídu pro rozšíření chování
    return class extends constructor {
      createdAt = new Date();
      // Další vlastnosti nebo metody pro všechny 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("--- Služby zaregistrovány ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Uživatelé:", userServiceInstance.getUsers());
  // console.log("User Service Created At:", userServiceInstance.createdAt); // Pokud se použije vrácená třída
}

Tento příklad ukazuje, jak může dekorátor třídy zaregistrovat třídu a dokonce upravit její konstruktor. Dekorátor Injectable činí třídu zjistitelnou teoretickým systémem injektáže závislostí.

2. Dekorátory metod

Dekorátory metod se aplikují na deklarace metod. Obdrží tři argumenty: cílový objekt (pro statické členy konstruktorová funkce; pro instanční členy prototyp třídy), název metody a deskriptor vlastnosti metody.

Signatura:

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

Návratová hodnota:

Dekorátor metody může vrátit nový PropertyDescriptor. Pokud tak učiní, tento deskriptor bude použit k definování metody. To vám umožňuje modifikovat nebo nahradit implementaci původní metody, což je neuvěřitelně mocné pro AOP.

Případy použití:

Příklad dekorátoru metody: Monitorování výkonu

Vytvořme dekorátor MeasurePerformance pro logování doby provádění metody.

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(`Metoda "${propertyKey}" se provedla za ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Simulace složité, časově náročné operace
    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 pro 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í metodu logikou pro měření času a vypíše dobu provádění, aniž by zahlcoval obchodní logiku uvnitř samotné metody. Toto je klasický příklad aspektově orientovaného programování (AOP).

3. Dekorátory přístupových metod (Accessor Decorators)

Dekorátory přístupových metod se aplikují na deklarace accessorů (get a set). Podobně jako dekorátory metod obdrží cílový objekt, název accessoru a jeho deskriptor vlastnosti.

Signatura:

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

Návratová hodnota:

Dekorátor přístupové metody může vrátit nový PropertyDescriptor, který bude použit k definování accessoru.

Případy použití:

Příklad dekorátoru přístupové metody: Cachování getterů

Vytvořme dekorátor, který ukládá do mezipaměti výsledek náročného výpočtu getteru.

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] Vypočítávám hodnotu pro ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Používám cachovanou hodnotu pro ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

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

  // Simuluje náročnou kalkulaci
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Provádím náročný výpočet souhrnu...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

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

console.log("První přístup:", generator.expensiveSummary);
console.log("Druhý přístup:", generator.expensiveSummary);
console.log("Třetí přístup:", generator.expensiveSummary);

Tento dekorátor zajišťuje, že výpočet getteru expensiveSummary proběhne pouze jednou, následná volání vracejí cachovanou hodnotu. Tento vzor je velmi užitečný pro optimalizaci výkonu tam, kde přístup k vlastnosti zahrnuje náročný výpočet nebo externí volání.

4. Dekorátory vlastností

Dekorátory vlastností se aplikují na deklarace vlastností. Obdrží dva argumenty: cílový objekt (pro statické členy konstruktorová funkce; pro instanční členy prototyp třídy) a název vlastnosti.

Signatura:

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

Návratová hodnota:

Dekorátory vlastností nemohou vracet žádnou hodnotu. Jejich primárním využitím je registrace metadat o vlastnosti. Nemohou přímo měnit hodnotu vlastnosti nebo její deskriptor v době dekorace, protože deskriptor pro vlastnost ještě není plně definován, když jsou dekorátory vlastností spuštěny.

Případy použití:

Příklad dekorátoru vlastnosti: Validace povinného pole

Vytvořme dekorátor pro označení vlastnosti jako „povinné“ a její následnou validaci za běhu.

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)} je povinná položka.`
  });
  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("Validační chyby uživatele 1:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("Validační chyby uživatele 2:", validate(user2)); // ["firstName je povinná položka."]

const user3 = new UserProfile("Alice", "");
console.log("Validační chyby uživatele 3:", validate(user3)); // ["lastName je povinná položka."]

Dekorátor Required jednoduše registruje validační pravidlo v centrální mapě validationRules. Samostatná funkce validate pak používá tato metadata ke kontrole instance za běhu. Tento vzor odděluje validační logiku od definice dat, což ji činí znovu použitelnou a čistou.

5. Dekorátory parametrů

Dekorátory parametrů se aplikují na parametry v konstruktoru třídy nebo v metodě. Obdrží tři argumenty: cílový objekt (pro statické členy konstruktorová funkce; pro instanční členy prototyp třídy), název metody (nebo undefined pro parametry konstruktoru) a pořadový index parametru v seznamu parametrů funkce.

Signatura:

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

Návratová hodnota:

Dekorátory parametrů nemohou vracet žádnou hodnotu. Stejně jako dekorátory vlastností, jejich primární rolí je přidávat metadata o parametru.

Případy použití:

Příklad dekorátoru parametru: Injektování dat z požadavku

Simulujme, jak by webový framework mohl použít dekorátory parametrů k injektování specifických dat do parametru metody, například ID uživatele z požadavku.

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á funkce frameworku pro volání metody s vyřešenými parametry
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(`Načítám uživatele s ID: ${userId}, Token: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Mažu uživatele s ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// Simulace příchozího požadavku
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("\n--- Spouštím getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("\n--- Spouštím deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

Tento příklad ukazuje, jak mohou dekorátory parametrů shromažďovat informace o požadovaných parametrech metody. Framework pak může použít tato shromážděná metadata k automatickému vyřešení a injektování příslušných hodnot při volání metody, což výrazně zjednodušuje logiku controlleru nebo služby.

Skládání dekorátorů a pořadí provádění

Dekorátory lze aplikovat v různých kombinacích a porozumění jejich pořadí provádění je klíčové pro předvídání chování a vyhýbání se neočekávaným problémům.

Více dekorátorů na jednom cíli

Když je na jednu deklaraci (např. třídu, metodu nebo vlastnost) aplikováno více dekorátorů, provádějí se v určitém pořadí: zdola nahoru, nebo zprava doleva, pro jejich vyhodnocení. Jejich výsledky se však aplikují v opačném pořadí.

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

Zde bude nejprve vyhodnocen DecoratorB, poté DecoratorA. Pokud modifikují třídu (např. vrácením nového konstruktoru), modifikace z DecoratorA obalí nebo se aplikuje nad modifikací z DecoratorB.

Příklad: Řetězení dekorátorů metod

Uvažujme dva dekorátory metod: LogCall a Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Volám ${String(propertyKey)} s argumenty:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Metoda ${String(propertyKey)} vrátila:`, 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"]; // Simulace načítání rolí aktuálního uživatele
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Přístup odepřen pro ${String(propertyKey)}. Požadované role: ${roles.join(", ")}`);
        throw new Error("Neoprávněný přístup");
      }
      console.log(`[AUTH] Přístup povolen pro ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Mažu citlivá data pro ID: ${id}`);
    return `Data ID ${id} smazána.`;
  }

  @Authorization(["user"])
  @LogCall // Pořadí je zde změněno
  fetchPublicData(query: string) {
    console.log(`Načítám veřejná data s dotazem: ${query}`);
    return `Veřejná data pro dotaz: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- Volání deleteSensitiveData (Admin User) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- Volání fetchPublicData (Non-Admin User) ---");
  // Simulace neadministrátorského uživatele, který se snaží přistoupit k fetchPublicData, která vyžaduje roli 'user'
  const mockUserRoles = ["guest"]; // Toto selže při autorizaci
  // Aby to bylo dynamické, potřebovali byste DI systém nebo statický kontext pro role aktuálního uživatele.
  // Pro zjednodušení předpokládáme, že dekorátor Authorization má přístup k kontextu aktuálního uživatele.
  // Upravme dekorátor Authorization tak, aby pro účely dema vždy předpokládal roli 'admin',
  // takže první volání uspěje a druhé selže, abychom ukázali různé cesty.
  
  // Spusťte znovu s rolí 'user', aby fetchPublicData uspělo.
  // Představte si, že currentUserRoles v Authorization se stane: ['user']
  // Pro tento příklad to zjednodušme a ukažme efekt pořadí.
  service.fetchPublicData("search term"); // Toto provede Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Očekávaný výstup pro deleteSensitiveData:
[AUTH] Přístup povolen pro deleteSensitiveData
[LOG] Volám deleteSensitiveData s argumenty: [ 'record123' ]
Mažu citlivá data pro ID: record123
[LOG] Metoda deleteSensitiveData vrátila: Data ID record123 smazána.
*/

/* Očekávaný výstup pro fetchPublicData (pokud má uživatel roli 'user'):
[LOG] Volám fetchPublicData s argumenty: [ 'search term' ]
[AUTH] Přístup povolen pro fetchPublicData
Načítám veřejná data s dotazem: search term
[LOG] Metoda fetchPublicData vrátila: Veřejná data pro dotaz: search term
*/

Všimněte si pořadí: pro deleteSensitiveData se nejprve spustí Authorization (dole), poté ho obalí LogCall (nahoře). Vnitřní logika Authorization se provede první. Pro fetchPublicData se nejprve spustí LogCall (dole), poté ho obalí Authorization (nahoře). To znamená, že aspekt LogCall bude vně aspektu Authorization. Tento rozdíl je klíčový pro průřezové záležitosti, jako je logování nebo zpracování chyb, kde pořadí provádění může výrazně ovlivnit chování.

Pořadí provádění pro různé cíle

Když má třída, její členové a parametry dekorátory, pořadí provádění je dobře definováno:

  1. Dekorátory parametrů se aplikují jako první, pro každý parametr, počínaje od posledního parametru k prvnímu.
  2. Poté se aplikují dekorátory metod, přístupových metod nebo vlastností pro každého člena.
  3. Nakonec se aplikují dekorátory tříd na samotnou třídu.

V rámci každé kategorie se více dekorátorů na stejném cíli aplikuje zdola nahoru (nebo zprava doleva).

Příklad: Úplné pořadí provádění

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Dekorátor parametru: ${message} na parametru #${descriptorOrIndex} z ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Dekorátor metody/accessoru: ${message} na ${String(propertyKey)}`);
      } else {
        console.log(`Dekorátor vlastnosti: ${message} na ${String(propertyKey)}`);
      }
    } else {
      console.log(`Dekorátor třídy: ${message} na ${target.name}`);
    }
    return descriptorOrIndex; // Vraťte deskriptor pro metodu/accessor, undefined pro ostatní
  };
}

@log("Úroveň třídy D")
@log("Úroveň třídy C")
class MyDecoratedClass {
  @log("Statická vlastnost A")
  static staticProp: string = "";

  @log("Instanční vlastnost B")
  instanceProp: number = 0;

  @log("Metoda D")
  @log("Metoda C")
  myMethod(
    @log("Parametr Z") paramZ: string,
    @log("Parametr Y") paramY: number
  ) {
    console.log("Metoda myMethod provedena.");
  }

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

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

  constructor() {
    console.log("Konstruktor proveden.");
  }
}

new MyDecoratedClass();
// Zavolejte metodu pro spuštění dekorátoru metody
new MyDecoratedClass().myMethod("hello", 123);

/* Předpokládané pořadí výstupu (přibližné, v závislosti na konkrétní verzi TypeScriptu a kompilaci):
Dekorátor parametru: Parametr Y na parametru #1 z myMethod
Dekorátor parametru: Parametr Z na parametru #0 z myMethod
Dekorátor vlastnosti: Statická vlastnost A na staticProp
Dekorátor vlastnosti: Instanční vlastnost B na instanceProp
Dekorátor metody/accessoru: Getter/Setter F na myAccessor
Dekorátor metody/accessoru: Metoda C na myMethod
Dekorátor metody/accessoru: Metoda D na myMethod
Dekorátor třídy: Úroveň třídy C na MyDecoratedClass
Dekorátor třídy: Úroveň třídy D na MyDecoratedClass
Konstruktor proveden.
Metoda myMethod provedena.
*/

Přesné časování výpisu do konzole se může mírně lišit v závislosti na tom, kdy je volán konstruktor nebo metoda, ale pořadí, ve kterém se samotné funkce dekorátorů provádějí (a tedy i jejich vedlejší efekty nebo vrácené hodnoty aplikují), se řídí výše uvedenými pravidly.

Praktické aplikace a návrhové vzory s dekorátory

Dekorátory, zejména ve spojení s polyfillem reflect-metadata, otevírají novou sféru programování řízeného metadaty. To umožňuje výkonné návrhové vzory, které abstrahují boilerplate kód a průřezové záležitosti.

1. Injektáž závislostí (DI)

Jedním z nejvýznamnějších použití dekorátorů je v rámci frameworků pro injektáž závislostí (jako je @Injectable(), @Component() atd. v Angularu nebo rozsáhlé využití DI v NestJS). Dekorátory vám umožňují deklarovat závislosti přímo v konstruktorech nebo na vlastnostech, což frameworku umožňuje automaticky instanciovat a poskytovat správné služby.

Příklad: Zjednodušená injektáž služby

import "reflect-metadata"; // Nezbytné pro 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(`Třída ${target.name} není označena jako @Injectable.`);
    }

    // Získání typů parametrů konstruktoru (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žijte explicitní @Inject token, pokud je poskytnut, jinak odvoďte typ
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Nelze vyřešit parametr na indexu ${index} pro ${target.name}. Může se jednat o cyklickou závislost nebo primitivní typ bez explicitního @Inject.`);
      }
      return Container.resolve(token);
    });

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

// Definice služeb
@Injectable()
class DatabaseService {
  connect() {
    console.log("Připojuji se k databázi...");
    return "DB připojení";
  }
}

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

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

  login() {
    console.log(`AuthService: Autentizuji pomocí ${this.db.connect()}`);
    return "Uživatel přihlášen";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Příklad injektování přes vlastnost pomocí vlastního dekorátoru nebo funkce 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: Načítám profil uživatele...");
    return { id: 1, name: "Global User" };
  }
}

// Vyřešení hlavní služby
console.log("--- Řeším UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Řeším AuthService (měla by být cachovaná) ---");
const authService = Container.resolve(AuthService);
authService.login();

Tento propracovaný příklad ukazuje, jak dekorátory @Injectable a @Inject v kombinaci s reflect-metadata umožňují vlastnímu Containeru automaticky řešit a poskytovat závislosti. Metadata design:paramtypes automaticky emitovaná TypeScriptem (když je emitDecoratorMetadata nastaveno na true) jsou zde klíčová.

2. Aspektově orientované programování (AOP)

AOP se zaměřuje na modularizaci průřezových záležitostí (např. logování, bezpečnost, transakce), které procházejí napříč více třídami a moduly. Dekorátory jsou vynikajícím nástrojem pro implementaci konceptů AOP v TypeScriptu.

Příklad: Logování s dekorátorem metody

Vrátíme-li se k dekorátoru LogCall, je to dokonalý příklad AOP. Přidává logovací chování k jakékoli metodě, aniž by se měnil původní kód metody. Tím se odděluje „co dělat“ (obchodní logika) od „jak to dělat“ (logování, monitorování výkonu atd.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Vstupuji do metody: ${String(propertyKey)} s argumenty:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Opouštím metodu: ${String(propertyKey)} s výsledkem:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Chyba v metodě ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Částka platby musí být kladná.");
    }
    console.log(`Zpracovávám platbu ${amount} ${currency}...`);
    return `Platba ${amount} ${currency} úspěšně zpracována.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Vracím platbu pro transakční ID: ${transactionId}...`);
    return `Vrácení platby pro ${transactionId} zahájeno.`;
  }
}

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

Tento přístup udržuje třídu PaymentProcessor zaměřenou čistě na logiku plateb, zatímco dekorátor LogMethod se stará o průřezovou záležitost logování.

3. Validace a transformace

Dekorátory jsou neuvěřitelně užitečné pro definování validačních pravidel přímo na vlastnostech nebo pro transformaci dat během serializace/deserializace.

Příklad: Validace dat s dekorátory vlastností

Příklad @Required výše to již demonstroval. Zde je další příklad s validací čí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)} musí být kladné číslo.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} musí mít maximálně ${maxLength} znaků.`);
  };
}

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("Chyby produktu 1:", Product.validate(product1)); // []

const product2 = new Product("Velmi dlouhý název produktu, který přesahuje limit padesáti znaků pro účely testování", 50);
console.log("Chyby produktu 2:", Product.validate(product2)); // ["name musí mít maximálně 50 znaků."]

const product3 = new Product("Kniha", -10);
console.log("Chyby produktu 3:", Product.validate(product3)); // ["price musí být kladné číslo."]

Toto nastavení vám umožňuje deklarativně definovat validační pravidla na vlastnostech vašeho modelu, což činí vaše datové modely samopopisnými z hlediska jejich omezení.

Osvědčené postupy a úvahy

Ačkoli jsou dekorátory mocné, měly by být používány uvážlivě. Jejich nesprávné použití může vést ke kódu, který je těžší ladit nebo pochopit.

Kdy používat dekorátory (a kdy ne)

Dopady na výkon

Dekorátory se provádějí v době kompilace (nebo v době definice v běhovém prostředí JavaScriptu, pokud jsou transpiloány). Transformace nebo sběr metadat se děje při definici třídy/metody, nikoli při každém volání. Proto je dopad na běhový výkon *aplikování* dekorátorů minimální. Avšak *logika uvnitř* vašich dekorátorů může mít dopad na výkon, zejména pokud provádějí náročné operace při každém volání metody (např. složité výpočty v rámci dekorátoru metody).

Udržovatelnost a čitelnost

Dekorátory, pokud jsou použity správně, mohou výrazně zlepšit čitelnost přesunutím boilerplate kódu mimo hlavní logiku. Pokud však provádějí složité, skryté transformace, může se ladění stát náročným. Ujistěte se, že vaše dekorátory jsou dobře zdokumentované a jejich chování je předvídatelné.

Experimentální status a budoucnost dekorátorů

Je důležité znovu zdůraznit, že TypeScript dekorátory jsou založeny na návrhu TC39 ve fázi 3. To znamená, že specifikace je z velké části stabilní, ale stále může projít drobnými změnami, než se stane součástí oficiálního standardu ECMAScript. Frameworky jako Angular je přijaly a sázejí na jejich případnou standardizaci. To s sebou nese určitou míru rizika, i když vzhledem k jejich širokému přijetí jsou významné zlomové změny nepravděpodobné.

Návrh TC39 se vyvíjel. Současná implementace TypeScriptu je založena na starší verzi návrhu. Existuje rozdíl mezi „Legacy Decorators“ a „Standard Decorators“. Až oficiální standard přistane, TypeScript pravděpodobně aktualizuje svou implementaci. Pro většinu vývojářů používajících frameworky bude tento přechod spravován samotným frameworkem. Pro autory knihoven může být nutné porozumět jemným rozdílům mezi starými a budoucími standardními dekorátory.

Volba kompilátoru emitDecoratorMetadata

Tato volba, pokud je nastavena na true v tsconfig.json, instruuje kompilátor TypeScriptu, aby emitoval určitá metadata o typech z doby návrhu do zkompilovaného JavaScriptu. Tato metadata zahrnují typ parametrů konstruktoru (design:paramtypes), návratový typ metod (design:returntype) a typ vlastností (design:type).

Tato emitovaná metadata nejsou součástí standardního běhového prostředí JavaScriptu. Obvykle je konzumuje polyfill reflect-metadata, který je pak zpřístupňuje prostřednictvím funkcí Reflect.getMetadata(). To je naprosto klíčové pro pokročilé vzory, jako je injektáž závislostí, kde kontejner potřebuje znát typy závislostí, které třída vyžaduje, bez explicitní konfigurace.

Pokročilé vzory s dekorátory

Dekorátory lze kombinovat a rozšiřovat pro vytváření ještě sofistikovanějších vzorů.

1. Dekorování dekorátorů (Higher-Order Decorators)

Můžete vytvářet dekorátory, které modifikují nebo skládají jiné dekorátory. To je méně časté, ale ukazuje funkční povahu dekorátorů.

// Dekorátor, který zajistí, že metoda je logována a zároveň vyžaduje administrátorské role
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Nejprve aplikujte Authorization (vnitřní)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Poté aplikujte LogCall (vnější)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Vraťte upravený deskriptor
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Mažu uživatelský účet: ${userId}`);
    return `Uživatel ${userId} smazán.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Očekávaný výstup (za předpokladu administrátorské role):
[AUTH] Přístup povolen pro deleteUserAccount
[LOG] Volám deleteUserAccount s argumenty: [ 'user007' ]
Mažu uživatelský účet: user007
[LOG] Metoda deleteUserAccount vrátila: Uživatel user007 smazán.
*/

Zde je AdminAndLoggedMethod továrna, která vrací dekorátor, a uvnitř tohoto dekorátoru aplikuje dva další dekorátory. Tento vzor může zapouzdřit složité kompozice dekorátorů.

2. Použití dekorátorů pro mixiny

Ačkoli TypeScript nabízí jiné způsoby implementace mixinů, dekorátory lze použít k injektování schopností do tříd deklarativním způsobem.

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("Objekt uvolněn.");
  }
}

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

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Tyto vlastnosti/metody jsou injektovány dekorátorem
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Zdroj ${this.name} vytvořen.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Zdroj ${this.name} uklizen.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Je uvolněn: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Je uvolněn: ${resource.isDisposed}`);

Tento dekorátor @ApplyMixins dynamicky kopíruje metody a vlastnosti z bázových konstruktorů do prototypu odvozené třídy, čímž efektivně „vmíchává“ funkcionality.

Závěr: Posílení moderního vývoje v TypeScriptu

TypeScript dekorátory jsou výkonnou a expresivní funkcí, která umožňuje nové paradigma programování řízeného metadaty a aspektově orientovaného programování. Umožňují vývojářům vylepšovat, modifikovat a přidávat deklarativní chování do tříd, metod, vlastností, přístupových metod a parametrů, aniž by se měnila jejich základní logika. Toto oddělení záležitostí vede k čistšímu, udržovatelnějšímu a vysoce znovupoužitelnému kódu.

Od zjednodušení injektáže závislostí a implementace robustních validačních systémů až po přidávání průřezových záležitostí, jako je logování a monitorování výkonu, poskytují dekorátory elegantní řešení mnoha běžných vývojářských výzev. Zatímco jejich experimentální status vyžaduje obezřetnost, jejich široké přijetí v hlavních frameworcích značí jejich praktickou hodnotu a budoucí relevanci.

Zvládnutím TypeScript dekorátorů získáte významný nástroj do svého arzenálu, který vám umožní vytvářet robustnější, škálovatelnější a inteligentnější aplikace. Přijměte je zodpovědně, pochopte jejich mechaniku a odemkněte novou úroveň deklarativní síly ve svých projektech v TypeScriptu.