Decoratori în TypeScript
Un decorator este o funcție specială pe care o atașezi unei clase, unei metode, unei proprietăți sau unui accesor folosind sintaxa @name. El rulează atunci când clasa este definită și poate adăuga metadate, poate schimba sau poate înlocui lucrul pe care îl decorează.
Decoratorii sunt folosiți intens în framework-uri precum Angular și NestJS pentru injectarea dependențelor, rutare și altele.
Activarea decoratorilor
TypeScript modern (5.0 și mai nou) acceptă din start sintaxa standard (TC39) pentru decoratori — nu este nevoie de nicio opțiune de compilare, iar aceasta este forma folosită de toate exemplele din această pagină.
Codul mai vechi folosește designul legacy al decoratorilor. Ca să compilezi acea formă, trebuie să o activezi în tsconfig.json:
"compilerOptions": {"target": "ES2020","module": "commonjs","experimentalDecorators": true,"emitDecoratorMetadata": true,"strictPropertyInitialization": false},"include": ["src/**/*.ts"]
Notă: emitDecoratorMetadata face compilatorul să emită informații despre tipuri pentru decoratorii legacy. Biblioteci precum TypeORM și class-validator se bazează pe el.
Feluri de decoratori
TypeScript îți permite să decorezi mai multe feluri de declarații. Cu decoratorii standard, fiecare decorator primește două argumente: valoarea decorată și un obiect de context care îți spune felul și numele ei.
- Decorator de clasă — se aplică declarațiilor de clasă:
(constructor: Function, context: ClassDecoratorContext) => void | Function - Decorator de metodă — se aplică metodelor unei clase:
(method: Function, context: ClassMethodDecoratorContext) => void | Function - Decorator de accesor — se aplică câmpurilor
accessor(modul standard de a intercepta citirile și scrierile unei proprietăți):(target: { get, set }, context: ClassAccessorDecoratorContext) => { get?, set?, init? } - Decorator de câmp — se aplică proprietăților simple ale unei clase:
(value: undefined, context: ClassFieldDecoratorContext) => void | ((initialValue) => any)
Designul legacy trimite în schimb (target, propertyKey, descriptor) decoratorilor de metodă, (target, propertyKey) decoratorilor de proprietate și (target, propertyKey, parameterIndex) decoratorilor de parametru. Decoratorii de parametru există doar în designul legacy.
Decoratori de clasă
Un decorator de clasă primește constructorul clasei. Îl poți folosi ca să observi clasa, ca să o schimbi sau chiar ca să returnezi un constructor nou care o înlocuiește.
El este apelat o singură dată, atunci când clasa este declarată — nu de fiecare dată când creezi o instanță.
Decorator de clasă de bază
Acest decorator simplu afișează un mesaj în momentul în care clasa este definită.
logClass care primește constructorul clasei și un ClassDecoratorContext și afișează Clasa <name> a fost definită la <marcaj de timp ISO>. Aplică-l cu @logClass unei clase UserService care are o metodă getUsers() ce returnează ["Alice", "Bob", "Charlie"]. Nimic altceva nu este apelat — linia din jurnal apare de îndată ce fișierul este încărcat.
Ieșire: program TypeScript și rezultatul capturat
// Un decorator de clasă simplu care afișează definirea clasei
function logClass(constructor: Function, context: ClassDecoratorContext) {
console.log(`Clasa ${constructor.name} a fost definită la ${new Date().toISOString()}`);
}
// Aplicarea decoratorului
@logClass
class UserService {
getUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}
// Rezultatul afișat la încărcarea fișierului: „Clasa UserService a fost definită la [marcaj de timp]”
Decorator de clasă cu modificarea constructorului
Acest exemplu schimbă o clasă: adaugă o proprietate version și returnează un constructor învelitor care afișează un mesaj de fiecare dată când este creată o instanță.
versioned(version: string), care returnează un decorator de clasă ce setează prototype.version și apoi returnează un constructor învelitor care afișează Se creează o instanță de <name> v<version> și apelează constructorul original cu new, copiind prototipul, ca instanceof să funcționeze în continuare. Aplică @versioned("1.0.0") unei clase ApiClient cu o metodă fetchData() care afișează Se preiau datele...; creează o instanță, afișează version-ul ei (convertit la any) și apelează fetchData().
Ieșire: program TypeScript și rezultatul capturat
// Un decorator care adaugă o proprietate version și afișează crearea instanțelor
function versioned(version: string) {
return function (constructor: Function, context: ClassDecoratorContext) {
// Adaugă o proprietate statică
constructor.prototype.version = version;
// Păstrează constructorul original
const original: any = constructor;
// Creează un constructor nou care îl învelește pe cel original
const newConstructor: any = function (...args: any[]) {
console.log(`Se creează o instanță de ${original.name} v${version}`);
return new original(...args);
};
// Copiază prototipul, ca instanceof să funcționeze
newConstructor.prototype = original.prototype;
return newConstructor;
};
}
// Aplicarea decoratorului cu o versiune
@versioned('1.0.0')
class ApiClient {
fetchData() {
console.log('Se preiau datele...');
}
}
const client = new ApiClient();
console.log((client as any).version); // Afișează: 1.0.0
client.fetchData();
Decorator de clasă sigilată
Acest decorator împiedică adăugarea de proprietăți noi la clasă și marchează proprietățile existente ca neconfigurabile.
sealed care afișează Se sigilează <name>... și apelează Object.seal atât pe constructor, cât și pe prototipul lui. Aplică-l cu @sealed unei clase Greeter care are o proprietate greeting: string setată în constructor și o metodă greet() ce returnează Salut, <greeting>. Lasă o linie comentată care arată că adăugarea lui Greeter.prototype.newMethod ar eșua.
Ieșire: program TypeScript și rezultatul capturat
function sealed(constructor: Function, context: ClassDecoratorContext) {
console.log(`Se sigilează ${constructor.name}...`);
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return `Salut, ${this.greeting}`;
}
}
// Acest lucru va arunca o eroare în modul strict
// Greeter.prototype.newMethod = function() {}; // Eroare: nu se poate adăuga proprietatea newMethod
Decoratori de metodă
Un decorator de metodă este pus pe o metodă și o poate observa, schimba sau înlocui.
El primește doi parametri:
originalMethod: funcția metodei în sinecontext: unClassMethodDecoratorContextcu numele metodei (name), cu informația dacă ea estestaticsauprivateși cu un hookaddInitializer
Dacă decoratorul returnează o funcție, acea funcție înlocuiește metoda. Învelirea metodei originale în acest fel este modul în care adaugi jurnalizare, măsurarea timpului sau verificări în jurul unui apel.
Puncte cheie despre decoratorii de metodă:
- Rulează atunci când metoda este definită, nu atunci când este apelată
- Pot schimba comportamentul metodei învelind-o cu logică suplimentară
- Sunt folosiți pentru aspecte transversale, precum jurnalizarea, validarea și autorizarea
- Trebuie să returneze o funcție nouă sau
undefined(atunci când nu se înlocuiește nimic)
Decorator pentru măsurarea timpului unei metode
Acest decorator măsoară și afișează cât durează execuția unei metode.
measureTime(originalMethod, context: ClassMethodDecoratorContext), care returnează un înveliș ce reține performance.now() înainte și după apelul metodei originale și afișează <metodă> s-a executat în <ms>ms, cu două zecimale. Aplică-l pe processData(data: number[]): number[] dintr-o clasă DataProcessor care parcurge o buclă de 100.000.000 de ori ca să simuleze o prelucrare și returnează fiecare valoare dublată; apelează-l cu [1, 2, 3, 4, 5].
Ieșire: program TypeScript și rezultatul capturat
// Decorator de metodă pentru măsurarea timpului de execuție
function measureTime(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} s-a executat în ${(end - start).toFixed(2)}ms`);
return result;
};
}
// Folosirea decoratorului
class DataProcessor {
@measureTime
processData(data: number[]): number[] {
// Simulează timpul de prelucrare
for (let i = 0; i < 100000000; i++) { /* prelucrare */ }
return data.map(x => x * 2);
}
}
// Atunci când este apelată, va afișa timpul de execuție
const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);
Decorator de autorizare a metodei
Acest exemplu implementează controlul accesului pe bază de roluri: decoratorul verifică rolurile utilizatorului curent înainte de a lăsa metoda să ruleze.
type UserRole = "admin" | "editor" | "viewer" și un obiect currentUser (id 1, name John Doe, roles ["viewer"]). Scrie AllowedRoles(...roles), care returnează un decorator de metodă al cărui înveliș aruncă Utilizatorul <name> nu este autorizat să apeleze <metodă> atunci când utilizatorul curent nu are niciunul dintre roluri. Într-o clasă DocumentService, permite deleteDocument pentru admin/editor și viewDocument pentru toate trei rolurile; într-un try/catch apelează mai întâi vizualizarea, apoi ștergerea, ca viewer (a doua aruncă o eroare, iar mesajul ei este afișat cu console.error), apoi schimbă rolurile în ["admin"] și șterge din nou, de data aceasta cu succes.
Ieșire: program TypeScript și rezultatul capturat
// Rolurile utilizatorilor
type UserRole = 'admin' | 'editor' | 'viewer';
// Contextul utilizatorului curent (simplificat)
const currentUser = {
id: 1,
name: 'John Doe',
roles: ['viewer'] as UserRole[]
};
// Fabrică de decoratori pentru controlul accesului pe bază de roluri
function AllowedRoles(...allowedRoles: UserRole[]) {
return function (
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
const hasPermission = allowedRoles.some(role =>
currentUser.roles.includes(role)
);
if (!hasPermission) {
throw new Error(
`Utilizatorul ${currentUser.name} nu este autorizat să apeleze ${propertyKey}`
);
}
return originalMethod.apply(this, args);
};
};
}
// Folosirea decoratorului
class DocumentService {
@AllowedRoles('admin', 'editor')
deleteDocument(id: string) {
console.log(`Documentul ${id} a fost șters`);
}
@AllowedRoles('admin', 'editor', 'viewer')
viewDocument(id: string) {
console.log(`Se vizualizează documentul ${id}`);
}
}
// Utilizare
const docService = new DocumentService();
try {
docService.viewDocument('doc123'); // Funcționează - rolul viewer este permis
docService.deleteDocument('doc123'); // Aruncă o eroare - viewer nu poate șterge
} catch (error) {
console.error(error.message);
}
// Schimbă rolul utilizatorului în admin
currentUser.roles = ['admin'];
docService.deleteDocument('doc123'); // Acum funcționează - admin poate șterge
Decorator de avertizare pentru metode învechite
Acest decorator afișează un avertisment de învechire ori de câte ori este apelată o metodă care va fi eliminată într-o versiune viitoare.
deprecated(message: string), care returnează un decorator de metodă al cărui înveliș apelează console.warn cu Avertisment: <metodă> este învechită. <mesaj> și apoi metoda originală. Într-o clasă PaymentService, marchează processPayment(amount, currency) cu @deprecated("Folosește processPaymentV2 în schimb"), lasă processPaymentV2 nedecorată și apelează-le pe amândouă cu 100, "USD".
Ieșire: program TypeScript și rezultatul capturat
function deprecated(message: string) {
return function (
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
console.warn(`Avertisment: ${propertyKey} este învechită. ${message}`);
return originalMethod.apply(this, args);
};
};
}
class PaymentService {
@deprecated('Folosește processPaymentV2 în schimb')
processPayment(amount: number, currency: string) {
console.log(`Se procesează plata de ${amount} ${currency}`);
}
processPaymentV2(amount: number, currency: string) {
console.log(`Se procesează plata v2 de ${amount} ${currency}`);
}
}
const payment = new PaymentService();
payment.processPayment(100, 'USD'); // Afișează avertismentul de învechire
payment.processPaymentV2(100, 'USD'); // Fără avertisment
Decoratori de proprietate
Decoratorii de proprietate îți permit să observi sau să schimbi felul în care o proprietate este citită și scrisă.
Cu decoratorii standard, proprietatea este declarată cu cuvântul cheie accessor. Decoratorul primește apoi doi parametri:
target: un obiect cu funcțiilegetșisetgeneratecontext: unClassAccessorDecoratorContextcu numele proprietății (name)
Dacă returnezi un obiect cu propriile tale funcții get și set, acestea înlocuiesc accesorii impliciți.
Puncte cheie despre decoratorii de proprietate:
- Rulează atunci când clasa este definită, nu atunci când proprietatea este accesată
- Un decorator pus pe un câmp simplu (fără
accessor) poate doar să ajusteze valoarea inițială; ca să interceptezi citirile și scrierile, foloseșteaccessor - În designul legacy ei primesc
(target, propertyKey)și trebuie să foloseascăObject.definePropertyca să schimbe comportamentul - Sunt folosiți adesea pentru formatare, jurnalizare, validare sau metadate
- Pot fi combinați cu alți decoratori pentru un comportament mai complex
Decorator de proprietate formatată
Acest decorator formatează automat o proprietate de fiecare dată când i se atribuie o valoare.
format(formatString: string), care returnează un decorator de accesor ce păstrează o valoare privată value și returnează o pereche get/set în care setterul stochează formatString cu {} înlocuit de noua valoare. Aplică @format("Salut, {}!") pe accessor greeting: string dintr-o clasă Greeter, setează greeting la "Lume" și afișează-l (se așteaptă Salut, Lume!).
Ieșire: program TypeScript și rezultatul capturat
// Decorator de proprietate pentru formatarea unei proprietăți de tip string
function format(formatString: string) {
return function (target: ClassAccessorDecoratorTarget<any, string>, context: ClassAccessorDecoratorContext) {
let value: string;
const getter = () => value;
const setter = (newVal: string) => {
value = formatString.replace('{}', newVal);
};
return {
get: getter,
set: setter
};
};
}
class Greeter {
@format('Salut, {}!')
accessor greeting: string;
}
const greeter = new Greeter();
greeter.greeting = 'Lume';
console.log(greeter.greeting); // Afișează: Salut, Lume!
Decorator de jurnalizare a proprietății
Acest decorator afișează fiecare citire și fiecare schimbare a unei proprietăți.
logProperty, care returnează un getter ce afișează Se citește <name>: <valoare> și un setter ce afișează Se setează <name> de la <veche> la <nouă>. Aplică-l pe accessor name: string și pe accessor price: number dintr-o clasă Product al cărei constructor le atribuie pe amândouă. Creează new Product("Laptop", 999.99), setează price la 899.99, apoi afișează product.name.
Ieșire: program TypeScript și rezultatul capturat
function logProperty(target: ClassAccessorDecoratorTarget<any, any>, context: ClassAccessorDecoratorContext) {
const propertyKey = String(context.name);
let value: any;
const getter = function() {
console.log(`Se citește ${propertyKey}: ${value}`);
return value;
};
const setter = function(newVal: any) {
console.log(`Se setează ${propertyKey} de la ${value} la ${newVal}`);
value = newVal;
};
return {
get: getter,
set: setter
};
}
class Product {
@logProperty
accessor name: string;
@logProperty
accessor price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
const product = new Product('Laptop', 999.99);
product.price = 899.99; // Afișează: Se setează price de la 999.99 la 899.99
console.log(product.name); // Afișează: Se citește name: Laptop
Decorator pentru proprietate obligatorie
Acest decorator se asigură că o proprietate a primit o valoare înainte de a fi citită.
required, al cărui getter aruncă Proprietatea <name> este obligatorie atunci când valoarea stocată este undefined și al cărui setter doar stochează valoarea. Aplică-l pe accessor username: string și pe accessor email: string dintr-o clasă User (care are și un age?: number opțional) al cărei constructor le setează pe amândouă. Creează new User("johndoe", "john@example.com"), ceea ce funcționează, și lasă comentate liniile care arată că transmiterea lui undefined ar arunca o eroare. Programul nu afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
function required(target: ClassAccessorDecoratorTarget<any, any>, context: ClassAccessorDecoratorContext) {
const propertyKey = String(context.name);
let value: any;
const getter = function() {
if (value === undefined) {
throw new Error(`Proprietatea ${propertyKey} este obligatorie`);
}
return value;
};
const setter = function(newVal: any) {
value = newVal;
};
return {
get: getter,
set: setter
};
}
class User {
@required
accessor username: string;
@required
accessor email: string;
age?: number;
constructor(username: string, email: string) {
this.username = username;
this.email = email;
}
}
const user1 = new User('johndoe', 'john@example.com'); // Funcționează
// const user2 = new User(undefined, 'test@example.com'); // Aruncă eroare: Proprietatea username este obligatorie
// const user2 = new User('johndoe', undefined); // Aruncă eroare: Proprietatea email este obligatorie
Decoratori de parametru
În designul legacy al decoratorilor poți decora și un singur parametru al unui constructor sau al unei metode. Un astfel de decorator primește trei parametri:
target: prototipul clasei (pentru metodele de instanță) sau constructorul (pentru metodele statice)propertyKey: numele metodei (sauundefinedpentru parametrii constructorului)parameterIndex: poziția parametrului în lista de parametri
Decoratorii standard nu au decoratori de parametru. Exemplul de mai jos păstrează ideea — descrie câte o regulă pentru fiecare parametru, apoi verifică argumentele atunci când metoda este apelată — dar trimite regulile unui decorator de metodă, în ordinea parametrilor, așa că se compilează cu setările implicite.
Puncte cheie despre decoratorii de parametru:
- Rulează atunci când metoda este definită, nu atunci când este apelată
- De obicei sunt folosiți împreună cu un decorator de metodă care face munca propriu-zisă
- Cu biblioteca
reflect-metadatapot stoca și citi metadate - Sunt des întâlniți în framework-urile de injectare a dependențelor
- Indexul parametrului îți permite să găsești argumentul corespunzător la rulare
Decorator de validare a parametrilor
Acest exemplu validează parametrii unei metode: fiecare regulă spune ce tip este așteptat pe ce poziție, iar decoratorul de metodă verifică argumentele la fiecare apel.
validateParam(type), care returnează un obiect-regulă pentru "string" | "number" | "boolean", și un decorator de metodă validate(...rules) care transformă regulile în intrări { index, type } și, la fiecare apel, verifică fiecare argument (string nevid, number care nu este NaN, boolean) și aruncă Parametrul de la indexul <i> nu a trecut validarea <type> în caz de eșec. Aplică @validate(validateParam("string"), validateParam("number"), validateParam("boolean")) pe createUser(name, age, isActive) dintr-o clasă UserService care afișează Se creează utilizatorul: <name>, <age>, <isActive>; apelează-l cu "John", 30, true și lasă comentate apelurile care ar eșua.
Ieșire: program TypeScript și rezultatul capturat
type ParamType = 'string' | 'number' | 'boolean';
// Descrie o regulă pentru un parametru: ce tip este așteptat pe ce poziție
function validateParam(type: ParamType) {
return { type };
}
// Decorator de metodă: regulile sunt enumerate în aceeași ordine ca parametrii
function validate(...rules: { type: ParamType }[]) {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
const validations: Array<{index: number, type: string}> =
rules.map((rule, index) => ({ index, type: rule.type }));
return function (this: any, ...args: any[]) {
for (const validation of validations) {
const { index, type } = validation;
const param = args[index];
let isValid = false;
switch (type) {
case 'string':
isValid = typeof param === 'string' && param.length > 0;
break;
case 'number':
isValid = typeof param === 'number' && !isNaN(param);
break;
case 'boolean':
isValid = typeof param === 'boolean';
}
if (!isValid) {
throw new Error(`Parametrul de la indexul ${index} nu a trecut validarea ${type}`);
}
}
return originalMethod.apply(this, args);
};
};
}
class UserService {
@validate(validateParam('string'), validateParam('number'), validateParam('boolean'))
createUser(name: string, age: number, isActive: boolean) {
console.log(`Se creează utilizatorul: ${name}, ${age}, ${isActive}`);
}
}
const service = new UserService();
service.createUser('John', 30, true); // Funcționează
// service.createUser('', 30, true); // Aruncă eroare: Parametrul de la indexul 0 nu a trecut validarea string
// service.createUser('John', 'thirty' as any, true); // Aruncă eroare: Parametrul de la indexul 1 nu a trecut validarea number
Fabrici de decoratori
O fabrică de decoratori este o funcție care returnează un decorator.
Pentru că apelezi fabrica cu argumente, poți configura decoratorul pe care îl produce. Astfel decoratorii devin mai flexibili și mai ușor de reutilizat.
Decorator de jurnalizare configurabil
Acest exemplu construiește un decorator de jurnalizare care poate fi personalizat cu un nivel de jurnalizare și cu un mesaj.
logWithConfig(config: { level: "log" | "warn" | "error", message?: string }), care returnează un decorator de metodă al cărui înveliș afișează <mesaj>: <metodă> împreună cu argumentele, prin console[level], rulează metoda originală și apoi afișează <metodă> s-a încheiat. Aplică-l cu nivelul log și mesajul Se procesează plata metodei processPayment(amount: number) dintr-o clasă PaymentService care afișează Se procesează plata de $<amount>. Doar declară clasa — nimic nu este apelat, deci nu există nicio ieșire.
Ieșire: program TypeScript și rezultatul capturat
// Fabrică de decoratori care acceptă o configurație
function logWithConfig(config: { level: 'log' | 'warn' | 'error', message?: string }) {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
const propertyKey = String(context.name);
return function (this: any, ...args: any[]) {
const { level = 'log', message = 'Se execută metoda' } = config;
console[level](`${message}: ${propertyKey}`, { arguments: args });
const result = originalMethod.apply(this, args);
console[level](`${propertyKey} s-a încheiat`);
return result;
};
};
}
class PaymentService {
@logWithConfig({ level: 'log', message: 'Se procesează plata' })
processPayment(amount: number) {
console.log(`Se procesează plata de $${amount}`);
}
}
Ordinea de evaluare
Atunci când mai mulți decoratori sunt stivuiți pe o singură declarație, fabricile sunt evaluate de sus în jos, iar apoi decoratorii returnați de ele sunt apelați de jos în sus.
first() și second(), care afișează fiecare <nume>(): fabrică evaluată și returnează un decorator de metodă ce afișează <nume>(): apelat. Stivuiește @first() deasupra lui @second() pe o metodă goală method() din ExampleClass. Ordinea așteptată: mai întâi ambele fabrici, de sus în jos, apoi decoratorii, de jos în sus (second(): apelat înaintea lui first(): apelat).
Ieșire: program TypeScript și rezultatul capturat
function first() {
console.log('first(): fabrică evaluată');
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
console.log('first(): apelat');
};
}
function second() {
console.log('second(): fabrică evaluată');
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
console.log('second(): apelat');
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// Rezultat:
// first(): fabrică evaluată
// second(): fabrică evaluată
// second(): apelat
// first(): apelat
Exemple din lumea reală
Controler API cu decoratori
Acest exemplu folosește decoratori ca să descrie un mic controler API, asemănător cu ceea ce fac NestJS sau framework-urile bazate pe Express.
ROUTES. Scrie un decorator de clasă Controller(prefix) care stochează prefixul pe prototip și atribuie clasa rutelor colectate până atunci, și un decorator de metodă Get(path) care adaugă { method: "get", path, handler }. Decorează o clasă UserController cu @Controller("/users") și două metode, @Get("/") getAllUsers() și @Get("/:id") getUserById(id). O funcție registerRoutes() afișează Înregistrat GET <prefix><path> pentru fiecare rută.
Ieșire: program TypeScript și rezultatul capturat
// Implementări simple de decoratori (simplificate pentru exemplu)
const ROUTES: any[] = [];
function Controller(prefix: string = '') {
return function (constructor: Function, context: ClassDecoratorContext) {
constructor.prototype.prefix = prefix;
// Decoratorii de metodă rulează înaintea decoratorului de clasă, așa că
// rutele colectate până acum fără o țintă aparțin acestei clase
ROUTES.forEach(route => {
if (!route.target) {
route.target = constructor;
}
});
};
}
function Get(path: string = '') {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
ROUTES.push({
method: 'get',
path,
handler: originalMethod,
target: undefined
});
};
}
// Folosirea decoratorilor
@Controller('/users')
class UserController {
@Get('/')
getAllUsers() {
return { users: [{ id: 1, name: 'John' }] };
}
@Get('/:id')
getUserById(id: string) {
return { id, name: 'John' };
}
}
// Simulează înregistrarea rutelor
function registerRoutes() {
ROUTES.forEach(route => {
const prefix = route.target.prototype.prefix || '';
console.log(`Înregistrat ${route.method.toUpperCase()} ${prefix}${route.path}`);
});
}
registerRoutes();
// Rezultat:
// Înregistrat GET /users/
// Înregistrat GET /users/:id
Bune practici
Urmează aceste practici atunci când scrii decoratori:
- Păstrează decoratorii concentrați: fiecare decorator ar trebui să facă un singur lucru.
- Documentează comportamentul: spune clar ce face un decorator și ce efecte secundare are.
- Folosește fabrici de decoratori: fă decoratorii configurabili, ca să poată fi reutilizați.
- Ține cont de performanță: un decorator care învelește o metodă adaugă muncă la fiecare apel.
- Siguranța tipurilor: folosește tipurile de context (
ClassMethodDecoratorContextși celelalte) ca să păstrezi decoratorii siguri din punctul de vedere al tipurilor. - Tratarea erorilor: tratează corect erorile în interiorul decoratorilor.
- Testare: scrie teste unitare pentru decoratorii tăi.
- Metadate: folosește
reflect-metadatapentru cazurile avansate care au nevoie de informații despre tipuri la rulare.
Capcane frecvente
Evită aceste greșeli frecvente:
- Felul greșit de decorator: decoratorii în stil legacy au nevoie de
experimentalDecoratorsîntsconfig.json; decoratorii standard nu trebuie să îl aibă. - Semnătură incorectă a decoratorului: fiecare fel de decorator are proprii parametri. Cei greșiți duc la erori la rulare.
- Ordinea de evaluare: decoratorii stivuiți sunt aplicați de jos în sus.
- Inițializarea proprietăților: decoratorii de proprietate rulează înainte ca proprietățile de instanță să fie inițializate.
- Reflecția metadatelor: uiți să imporți
reflect-metadataatunci când folosești metadatele decoratorilor. - Costul de performanță: ai grijă cu decoratorii grei în cod critic pentru performanță.
- Compatibilitatea cu browserele: decoratorii sunt o propunere aflată în stadiul 3, așa că browserele mai vechi au nevoie de cod transpilat.
Cele două fragmente de mai jos folosesc sintaxa legacy. Ca să le compilezi, adaugă în tsconfig.json: "compilerOptions": { "experimentalDecorators": true }.
Unde sunt folosiți decoratorii?
- Angular: componente, servicii, module și altele
- NestJS: controlere, furnizori, rute și altele
Decorator legacy de proprietate readonly
Un decorator de proprietate în stil legacy care folosește Object.defineProperty ca să facă name nemodificabil.
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
class Person {
@readonly
name = "John";
}
Decorator legacy de parametru
Un decorator de parametru în stil legacy care afișează numele metodei și indexul parametrului decorat.
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter in ${propertyKey} at index ${parameterIndex}`);
}
class Demo {
greet(@logParameter message: string) {
return message;
}
}