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:
- Registrácia tried v kontajneri pre injektovanie závislostí.
- Aplikovanie mixinov alebo dodatočných funkcionalít na triedu.
- Konfigurácie špecifické pre framework (napr. routing vo webovom frameworku).
- Pridávanie lifecycle hookov do tried.
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:
- Logovanie volaní metód a ich argumentov/výsledkov.
- Kešovanie výsledkov metód na zlepšenie výkonu.
- Aplikovanie autorizačných kontrol pred vykonaním metódy.
- Meranie času vykonávania metódy.
- Debouncing alebo throttling volaní metód.
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:
- Validácia pri nastavovaní vlastnosti.
- Transformácia hodnoty pred jej nastavením alebo po jej získaní.
- Kontrola prístupových práv pre vlastnosti.
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:
- Registrácia vlastností pre serializáciu/deserializáciu.
- Aplikovanie validačných pravidiel na vlastnosti.
- Nastavenie predvolených hodnôt alebo konfigurácií pre vlastnosti.
- Mapovanie stĺpcov ORM (Object-Relational Mapping) (napr.
@Column()
v TypeORM).
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:
- Registrácia typov parametrov pre injektovanie závislostí (napr.
@Inject()
v Angular). - Aplikovanie validácie alebo transformácie na špecifické parametre.
- Extrahovanie metadát o parametroch API požiadaviek vo webových frameworkoch.
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é:
- Dekorátory parametrov sa aplikujú ako prvé, pre každý parameter, od posledného parametra po prvý.
- Potom sa aplikujú dekorátory metód, prístupových metód alebo vlastností pre každého člena.
- 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)
- Používajte ich na:
- Prierezové záležitosti: Logovanie, kešovanie, autorizácia, správa transakcií.
- Deklarácia metadát: Definovanie schém pre ORM, validačných pravidiel, konfigurácie DI.
- Integrácia s frameworkom: Pri vytváraní alebo používaní frameworkov, ktoré využívajú metadáta.
- Redukcia opakujúceho sa kódu: Abstrahovanie opakujúcich sa kódových vzorov.
- Vyhnite sa im pre:
- Jednoduché volania funkcií: Ak bežné volanie funkcie môže dosiahnuť rovnaký výsledok jasne, uprednostnite ho.
- Obchodná logika: Dekorátory by mali rozširovať, nie definovať, základnú obchodnú logiku.
- Zbytočné komplikovanie: Ak použitie dekorátora robí kód menej čitateľným alebo ťažšie testovateľným, prehodnoťte to.
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.