Treci la conținutul principal

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ă.

Prompt
Arată un decorator de clasă de bază, care rulează atunci când o clasă este declarată. Scrie un decorator 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

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ță.

Prompt
Arată o fabrică de decoratori de clasă care schimbă o clasă și îi înlocuiește constructorul. Scrie 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

Decorator de clasă sigilată

Acest decorator împiedică adăugarea de proprietăți noi la clasă și marchează proprietățile existente ca neconfigurabile.

Prompt
Arată un decorator de clasă care sigilează o clasă, astfel încât să nu i se mai poată adăuga membri noi. Scrie un decorator 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

Decoratori de metodă

Un decorator de metodă este pus pe o metodă și o poate observa, schimba sau înlocui.

El primește doi parametri:

  1. originalMethod: funcția metodei în sine
  2. context: un ClassMethodDecoratorContext cu numele metodei (name), cu informația dacă ea este static sau private și cu un hook addInitializer

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.

Prompt
Arată un decorator de metodă care măsoară cât durează rularea unei metode. Scrie 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 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.

Prompt
Arată controlul accesului pe bază de roluri, implementat cu o fabrică de decoratori de metodă. Definește 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

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.

Prompt
Arată o fabrică de decoratori de metodă care afișează un avertisment de învechire înainte de a rula metoda. Scrie 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

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:

  1. target: un obiect cu funcțiile get și set generate
  2. context: un ClassAccessorDecoratorContext cu 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ște accessor
  • În designul legacy ei primesc (target, propertyKey) și trebuie să folosească Object.defineProperty ca 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.

Prompt
Arată o fabrică de decoratori de proprietate (auto-accesor) care formatează o valoare de fiecare dată când aceasta este setată. Scrie 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 jurnalizare a proprietății

Acest decorator afișează fiecare citire și fiecare schimbare a unei proprietăți.

Prompt
Arată un decorator de accesor care afișează fiecare citire și fiecare scriere a unei proprietăți. Scrie 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

Decorator pentru proprietate obligatorie

Acest decorator se asigură că o proprietate a primit o valoare înainte de a fi citită.

Prompt
Arată un decorator de accesor care impune ca o proprietate să aibă o valoare înainte de a fi citită. Scrie 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

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:

  1. target: prototipul clasei (pentru metodele de instanță) sau constructorul (pentru metodele statice)
  2. propertyKey: numele metodei (sau undefined pentru parametrii constructorului)
  3. 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-metadata pot 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.

Prompt
Arată validarea parametrilor condusă de decoratori, scrisă cu decoratori standard (TC39), care nu au decoratori de parametru. Scrie 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

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.

Prompt
Arată o fabrică de decoratori care primește un obiect de configurare. Scrie 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

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.

Prompt
Arată ordinea în care sunt evaluate fabricile de decoratori și decoratorii returnați de ele, atunci când mai multe sunt stivuite pe o singură metodă. Scrie două fabrici, 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

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.

Prompt
Arată cum pot decoratorii să înregistreze rute pentru un mic controler API, în stilul NestJS sau Express. Păstrează un tablou 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

Bune practici

Urmează aceste practici atunci când scrii decoratori:

  1. Păstrează decoratorii concentrați: fiecare decorator ar trebui să facă un singur lucru.
  2. Documentează comportamentul: spune clar ce face un decorator și ce efecte secundare are.
  3. Folosește fabrici de decoratori: fă decoratorii configurabili, ca să poată fi reutilizați.
  4. Ține cont de performanță: un decorator care învelește o metodă adaugă muncă la fiecare apel.
  5. Siguranța tipurilor: folosește tipurile de context (ClassMethodDecoratorContext și celelalte) ca să păstrezi decoratorii siguri din punctul de vedere al tipurilor.
  6. Tratarea erorilor: tratează corect erorile în interiorul decoratorilor.
  7. Testare: scrie teste unitare pentru decoratorii tăi.
  8. Metadate: folosește reflect-metadata pentru 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 în tsconfig.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-metadata atunci 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.

script.ts
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.

script.ts
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter in ${propertyKey} at index ${parameterIndex}`);
}

class Demo {
  greet(@logParameter message: string) {
    return message;
  }
}