MethodDecorator în TypeScript

Tipurile decoratorilor vechi pentru metode, proprietăți și parametri, folosite cu experimentalDecorators.

Sintaxă
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol | undefined,
    parameterIndex: number) => void;

Ce face MethodDecorator

Cu opțiunea experimentalDecorators, un decorator de metodă primește trei argumente: target (prototipul clasei, sau constructorul pentru metodele statice), numele metodei și descriptorul proprietății. Schimbând descriptor.value, înlocuiești metoda. MethodDecorator este tipul acestei funcții; aceeași formă o au decoratorii de accesori get/set.

PropertyDecorator primește doar target și numele câmpului (nu și valoarea, care nu există încă) și este folosit de obicei pentru a înregistra metadate. ParameterDecorator primește în plus poziția parametrului; pentru parametrii constructorului, numele este undefined. Decoratorii de parametri există doar în varianta experimentală, unde sunt baza injecției de dependențe din Angular și NestJS.

Fără experimentalDecorators, TypeScript folosește decoratorii standard, care au altă semnătură, deci un MethodDecorator nu poate fi aplicat cu @ (vezi ultimul exemplu). Exemplele cu @ de mai jos sunt compilate cu tsc --experimentalDecorators.

Parametri

Parametru Descriere
target Prototipul clasei (pentru membri de instanță) sau constructorul (pentru membri statici și parametrii constructorului).
propertyKey Numele metodei, proprietății sau metodei care are parametrul.
descriptor Doar la MethodDecorator: descriptorul proprietății, cu value, writable etc.
parameterIndex Doar la ParameterDecorator: poziția parametrului, de la 0.

Ce returnează

MethodDecorator: un descriptor nou sau void. Celelalte: void.

Exemple

Exemplul 1: MethodDecorator care afișează apelurile

const logheaza: MethodDecorator = (target, cheie, descriptor) => {
    const original = descriptor.value as (...args: unknown[]) => unknown;
    descriptor.value = function (this: unknown, ...args: unknown[]) {
        console.log(`${target.constructor.name}.${String(cheie)}(${args.join(", ")})`);
        return original.apply(this, args);
    } as typeof descriptor.value;
    return descriptor;
};

class Catalog {
    @logheaza
    adaugaNota(elev: string, nota: number): string {
        return `${elev}: ${nota}`;
    }
}

console.log(new Catalog().adaugaNota("Ana", 10));

Rezultat (compilat cu –experimentalDecorators)

Catalog.adaugaNota(Ana, 10)
Ana: 10

Exemplul 2: PropertyDecorator și ParameterDecorator

Decoratorii de parametri rulează la definirea clasei, de la ultimul parametru spre primul, iar cei ai constructorului primesc undefined ca nume.

const campuri: string[] = [];
const obligatorii = new Map<string, number[]>();

const camp: PropertyDecorator = (target, cheie) => {
    campuri.push(`${target.constructor.name}.${String(cheie)}`);
};

const obligatoriu: ParameterDecorator = (target, cheie, index) => {
    const nume = cheie === undefined ? "constructor" : String(cheie);
    console.log(`parametrul ${index} din ${nume} (${(target as { name?: string }).name ?? target.constructor.name}) este obligatoriu`);
    obligatorii.set(nume, [...(obligatorii.get(nume) ?? []), index]);
};

class Elev {
    @camp nume: string;
    @camp nota: number;

    constructor(@obligatoriu nume: string, nota: number) {
        this.nume = nume;
        this.nota = nota;
    }

    modifica(@obligatoriu nota: number, @obligatoriu motiv: string): void {
        console.log(`nota ${this.nota} -> ${nota} (${motiv})`);
        this.nota = nota;
    }
}

console.log(campuri);
console.log(obligatorii);
new Elev("Ana", 9).modifica(10, "contestație");

Rezultat (compilat cu –experimentalDecorators)

parametrul 1 din modifica (Elev) este obligatoriu
parametrul 0 din modifica (Elev) este obligatoriu
parametrul 0 din constructor (Elev) este obligatoriu
[ 'Elev.nume', 'Elev.nota' ]
Map(2) { 'modifica' => [ 1, 0 ], 'constructor' => [ 0 ] }
nota 9 -> 10 (contestație)

Exemplul 3: Aplicat manual, fără @

Un MethodDecorator este o funcție obișnuită: îl poți apela tu cu prototipul, numele și descriptorul, exact cum face codul generat de tsc. Exemplul rulează fără experimentalDecorators.

const dublu: MethodDecorator = (_target, _cheie, descriptor) => {
    const original = descriptor.value as unknown as (x: number) => number;
    return { ...descriptor, value: ((x: number) => original(x) * 2) as typeof descriptor.value };
};

class Calcul {
    patrat(x: number): number {
        return x * x;
    }
}

const descriptor = Object.getOwnPropertyDescriptor(Calcul.prototype, "patrat")!;
const nou = dublu(Calcul.prototype, "patrat", descriptor) || descriptor;
Object.defineProperty(Calcul.prototype, "patrat", nou);

console.log(new Calcul().patrat(3));

Rezultat

18

Exemplul 4: MethodDecorator fără experimentalDecorators

Cu decoratorii standard, metoda este apelată cu două argumente (valoare, context), nu cu trei.

const logheaza: MethodDecorator = (_target, _cheie, descriptor) => descriptor;
class Catalog {
    @logheaza
    adauga(): void {}
}
new Catalog().adauga();

Eroarea afișată de tsc

exemplu.mts(3,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
  The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
exemplu.mts(3,6): error TS1270: Decorator function return type 'void | TypedPropertyDescriptor<unknown>' is not assignable to type 'void | (() => void)'.
  Type 'TypedPropertyDescriptor<unknown>' is not assignable to type 'void | (() => void)'.

Greșeli frecvente

  • Aplici un MethodDecorator cu @ fără experimentalDecorators în tsconfig.json. tsc dă Unable to resolve signature of method decorator.
  • Încerci să citești valoarea unui câmp într-un PropertyDecorator. Decoratorul rulează la definirea clasei, înainte să existe vreun obiect.

Comenzi înrudite

  • ClassDecorator: Tipul unui decorator de clasă în stilul vechi (experimentalDecorators): primește constructorul clasei.
  • decoratori: Funcții puse cu @ deasupra claselor, metodelor sau câmpurilor, care le modifică sau le înlocuiesc comportamentul.
  • accesori get/set: Proprietăți calculate: get rulează cod la citire, set rulează cod la scriere, dar se folosesc ca un câmp.