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í:
- Registrace tříd v kontejneru pro injektáž závislostí.
- Aplikování mixinů nebo dalších funkcionalit na třídu.
- Konfigurace specifické pro framework (např. routing ve webovém frameworku).
- Přidávání lifecycle hooků do tříd.
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í:
- Logování volání metod a jejich argumentů/výsledků.
- Ukládání výsledků metod do mezipaměti pro zlepšení výkonu.
- Aplikování autorizačních kontrol před spuštěním metody.
- Měření doby provádění metody.
- Debouncing nebo throttling volání metod.
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í:
- Validace při nastavování vlastnosti.
- Transformace hodnoty před jejím nastavením nebo po jejím načtení.
- Řízení přístupových oprávnění k vlastnostem.
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í:
- Registrace vlastností pro serializaci/deserializaci.
- Aplikování validačních pravidel na vlastnosti.
- Nastavení výchozích hodnot nebo konfigurací pro vlastnosti.
- Mapování sloupců pro ORM (Object-Relational Mapping) (např.
@Column()
v TypeORM).
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í:
- Registrace typů parametrů pro injektáž závislostí (např.
@Inject()
v Angularu). - Aplikování validace nebo transformace na specifické parametry.
- Extrahování metadat o parametrech API požadavků ve webových frameworcích.
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:
- Dekorátory parametrů se aplikují jako první, pro každý parametr, počínaje od posledního parametru k prvnímu.
- Poté se aplikují dekorátory metod, přístupových metod nebo vlastností pro každého člena.
- 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)
- Používejte je pro:
- Průřezové záležitosti: Logování, cachování, autorizace, správa transakcí.
- Deklaraci metadat: Definování schémat pro ORM, validačních pravidel, konfigurace DI.
- Integraci s frameworky: Při vytváření nebo používání frameworků, které využívají metadata.
- Redukci boilerplate kódu: Abstrakci opakujících se vzorů kódu.
- Vyhněte se jim pro:
- Jednoduchá volání funkcí: Pokud lze stejného výsledku dosáhnout jasně pomocí prostého volání funkce, upřednostněte to.
- Obchodní logiku: Dekorátory by měly rozšiřovat, nikoli definovat, základní obchodní logiku.
- Překomplikování: Pokud použití dekorátoru činí kód méně čitelným nebo hůře testovatelným, zvažte jiné řešení.
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.