decoratori în TypeScript

Clase TypeScript 5.0

Funcții puse cu @ deasupra claselor, metodelor sau câmpurilor, care le modifică sau le înlocuiesc comportamentul.

Sintaxă
function dec(valoare: Functie, context: ClassMethodDecoratorContext) {
    return function (this: unknown, ...args: unknown[]) { ... };
}
class A {
    @dec metoda() { ... }
}

Ce face decoratori

Un decorator este o funcție pe care o aplici cu @nume înaintea unei clase, metode, accesor sau câmp. Decoratorul primește elementul decorat și un obiect context (cu numele, felul elementului, addInitializer etc.) și poate returna un înlocuitor. Este felul în care framework-uri ca Angular sau NestJS adaugă comportament (logare, validare, rute) fără să schimbi codul metodei.

TypeScript 5.0 implementează decoratorii standard (propunerea ECMAScript, etapa 3). Tipurile contextului sunt ClassDecoratorContext, ClassMethodDecoratorContext, ClassFieldDecoratorContext, ClassAccessorDecoratorContext etc. Cuvântul accessor pe un câmp creează automat un getter și un setter, pe care un decorator le poate înlocui.

Proiectele mai vechi folosesc decoratorii „experimentali”, cu opțiunea experimentalDecorators (și eventual emitDecoratorMetadata). Au altă semnătură (de exemplu (target, cheie, descriptor)) și permit și decoratori de parametri; tipurile lor sunt ClassDecorator, MethodDecorator și celelalte. Cele două variante nu se amestecă într-un proiect.

Exemple

Exemplul 1: Decorator de metodă care afișează apelurile

logheaza primește metoda originală și returnează o funcție nouă care afișează argumentele și rezultatul.

function logheaza<This, Args extends unknown[], R>(
    original: (this: This, ...args: Args) => R,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
) {
    const nume = String(context.name);
    return function (this: This, ...args: Args): R {
        const rezultat = original.call(this, ...args);
        console.log(`${nume}(${args.join(", ")}) = ${rezultat}`);
        return rezultat;
    };
}

class Calculator {
    @logheaza
    aduna(a: number, b: number): number {
        return a + b;
    }
    @logheaza
    tva(pret: number): number {
        return Math.round(pret * 21) / 100;
    }
}

const c = new Calculator();
c.aduna(2, 3);
c.tva(50);

Rezultat

aduna(2, 3) = 5
tva(50) = 10.5

Exemplul 2: Decorator de clasă și addInitializer

Decoratorul de clasă înregistrează clasa într-o listă. Decoratorul @legat leagă metoda de obiect, ca să poată fi dată ca callback fără să piardă this.

const registru: string[] = [];

function inregistreaza(clasa: abstract new (...args: never[]) => unknown, context: ClassDecoratorContext) {
    registru.push(String(context.name));
    return clasa as never;
}

function legat(_metoda: (this: Elev) => string, context: ClassMethodDecoratorContext<Elev, (this: Elev) => string>) {
    context.addInitializer(function () {
        const m = this[context.name as "salut"];
        this[context.name as "salut"] = m.bind(this);
    });
}

@inregistreaza
class Elev {
    constructor(public nume: string) {}
    @legat
    salut(): string {
        return `Salut, sunt ${this.nume}`;
    }
}

const f = new Elev("Ana").salut;    // metoda luată separat de obiect
console.log(f());
console.log(registru);

Rezultat

Salut, sunt Ana
[ 'Elev' ]

Exemplul 3: Decorator pe un câmp accessor

accessor creează getter și setter; decoratorul înlocuiește setter-ul ca să limiteze nota între 1 și 10.

function intre(min: number, max: number) {
    return function <T>(
        tinta: ClassAccessorDecoratorTarget<T, number>,
        _context: ClassAccessorDecoratorContext<T, number>,
    ): ClassAccessorDecoratorResult<T, number> {
        return {
            set(this: T, v: number) {
                tinta.set.call(this, Math.min(max, Math.max(min, v)));
            },
        };
    };
}

class Nota {
    @intre(1, 10) accessor valoare = 5;
}

const n = new Nota();
n.valoare = 14;
console.log(n.valoare);
n.valoare = -2;
console.log(n.valoare);

Rezultat

10
1

Greșeli frecvente

  • Copiezi un decorator scris pentru experimentalDecorators (cu target, propertyKey, descriptor) într-un proiect cu decoratori standard. Semnăturile diferă și tsc dă erori de tip la aplicare.
  • Uiți că decoratorii de parametri (constructor(@Inject() x)) există doar în varianta experimentală.
  • Pui logică grea în decoratori pentru lucruri simple. Un apel de funcție obișnuit este adesea mai ușor de citit.

Comenzi înrudite

  • ClassDecorator: Tipul unui decorator de clasă în stilul vechi (experimentalDecorators): primește constructorul clasei.
  • MethodDecorator: Tipurile decoratorilor vechi pentru metode, proprietăți și parametri, folosite cu experimentalDecorators.
  • class: Clasele din JavaScript, cu tipuri pe câmpuri, constructor și metode, plus modificatori de acces și interfețe.
  • accesori get/set: Proprietăți calculate: get rulează cod la citire, set rulează cod la scriere, dar se folosesc ca un câmp.