Full Stack

Decoratori in TypeScript

Autore

Manuel Ricci

Nel mondo della programmazione moderna, TypeScript si è affermato come una delle principali estensioni di JavaScript, offrendo tipizzazione statica e altri strumenti utili per lo sviluppo di applicazioni robuste e manutenibili. Uno di questi strumenti potenti, ma a volte sottovalutati, sono i decoratori. In questo approfondimento, esploreremo il concetto dei decoratori in TypeScript, con un'attenzione particolare al loro uso pratico e alle loro limitazioni.

Cosa sono i decoratori

I decoratori sono una funzionalità sperimentale in TypeScript che consente di annotare e modificare classi e proprietà direttamente nella dichiarazione del codice. Sono utilizzati principalmente per la metaprogrammazione, ovvero per programmare programmi. In TypeScript, i decoratori forniscono un modo per aggiungere annotazioni e una logica di programmazione meta-level a classi, metodi, accessori, proprietà e parametri.

Prima di iniziare

Al momento della scrittura di questo articolo per poter usare i decoratori è necessario abilitare l’opzione experimentalDecorators nel file tsconfig.json.

Uso Pratico dei Decoratori in TypeScript

Decoratori di Classe: Usati per osservare, modificare o sostituire la definizione di una classe. Possono essere utili per aggiungere metadati, effettuare binding, dependency injection, o altro.

1@sealed
2class Greeter {
3    greeting: string;
4        constructor(message: string) {
5        this.greeting = message;
6    }
7    greet() {
8         return "Hello, " + this.greeting;
9    }
10}
Qui `@sealed` è un decoratore che potrebbe sigillare sia il costruttore che il suo prototipo.

Decoratori di Metodo: Applicati alle funzioni dei membri di una classe. Utili per modificare il comportamento del metodo, registrare informazioni, o introdurre logica aggiuntiva.

1   class MyClass {
2     @log
3     myMethod(arg: number) {
4       return "Message " + arg;
5     }
6   }

In questo caso, @log potrebbe essere usato per registrare l'invocazione del metodo.Più avanti vedremo un esempio più ricco.

Decoratori di Accesso: Applicati ai getter/setter di una proprietà. Possono essere utili per intercettare e modificare operazioni di lettura e scrittura.

Decoratori di Proprietà e Parametri: Utilizzati per gestire metadati aggiuntivi e comportamenti personalizzati per le proprietà o i parametri dei metodi.

Casi d’uso e limitazione dei decoratori

  1. Casi d'Uso:

    • Logging e Debugging: Per registrare automaticamente le chiamate ai metodi.
    • Validazione: Assicurare che i valori assegnati alle proprietà rispettino certi criteri.
    • Dependency Injection: In framework come Angular, i decoratori facilitano l'iniezione di dipendenze.
    • Mapping e Serializzazione: Automatizzare il mapping tra oggetti e la loro rappresentazione in formati come JSON.
  2. Limitazioni:

    • Standardizzazione: I decoratori sono ancora una funzionalità sperimentale in TypeScript e non sono pienamente standardizzati in ECMAScript, il che può portare a problemi di compatibilità.
    • Complessità e Prestazioni: L'uso eccessivo di decoratori può rendere il codice più difficile da comprendere e può avere un impatto sulle prestazioni.
    • Limitazioni Tecniche: Non tutti i tipi di decoratori sono supportati in ogni contesto. Ad esempio, i decoratori di proprietà non possono osservare le inizializzazioni all'interno del costruttore.

Esempi di decoratori in TypeScript

1function LogMethodCalls<T extends (...args: any[]) => any>(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
2  const originalMethod = descriptor.value;
3
4  descriptor.value = function (...args: Parameters<T>): ReturnType<T> {
5      console.log(`Chiamata al metodo: ${propertyKey} con parametri: ${JSON.stringify(args)}`);
6
7      const result = originalMethod!.apply(this, args);
8
9      console.log(`Risultato del metodo: ${propertyKey}: ${JSON.stringify(result)}`);
10      return result;
11  } as T;
12
13  return descriptor;
14}

In questo esempio, LogMethodCalls è un decoratore di metodo. Quando viene applicato a un metodo di una classe, registra il nome del metodo, i parametri passati e il valore restituito dal metodo.

Creiamo quindi una classe per applicare questo decoratore.

1class Calculator {
2  @LogMethodCalls
3  add(a: number, b: number): number {
4      return a + b;
5  }
6
7  @LogMethodCalls
8  multiply(a: number, b: number): number {
9      return a * b;
10  }
11}
12
13const calc = new Calculator();
14console.log(calc.add(5, 3));
15console.log(calc.multiply(10, 2));

In questa classe Calculator, abbiamo due metodi: add e multiply. Entrambi sono decorati con @LogMethodCalls. Quando questi metodi vengono invocati, il decoratore registrerà le informazioni relative alla loro chiamata.

Nel log quindi vedremo:

1Chiamata al metodo: add con parametri: [5,3]
2Risultato del metodo: add: 8
3Chiamata al metodo: multiply con parametri: [10,2]
4Risultato del metodo: multiply: 20

Un altro esempio di decoratore è quello per gestire la cache dei risultati di un metodo. Questo tipo di decoratore è particolarmente utile per ottimizzare le prestazioni memorizzando i risultati di operazioni costose in termini di tempo, come calcoli complessi o richieste di rete, per evitare di doverle ripetere.

Definiremo un decoratore CacheResult che memorizzerà il risultato di un metodo data una specifica combinazione di parametri. Se il metodo viene richiamato con la stessa combinazione di parametri, il decoratore fornirà il risultato dalla cache invece di eseguire nuovamente il metodo.

Ecco il codice per il decoratore CacheResult:

1function CacheResult<T extends ( ...args: any[] ) => any>( target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<T> ) {
2  const originalMethod = descriptor.value;
3  const cacheKey = Symbol.for( propertyName );
4
5
6  descriptor.value = function ( ...args: Parameters<T> ): ReturnType<T> | undefined {
7
8    if ( !this[cacheKey] ) {
9      this[cacheKey] = new Map();
10    }
11
12    const cache = this[cacheKey];
13    const key = JSON.stringify( args );
14
15    if ( !cache.has( key ) ) {
16      try {
17        const result = originalMethod!.apply( this, args );
18        cache.set( key, result );
19      } catch ( error ) {
20        console.error( `Error executing method ${ propertyName }: ${ error }` );
21        return undefined;
22      }
23    }
24
25    return cache.get( key );
26  } as T;
27
28  return descriptor;
29}

In questo esempio, CacheResult memorizza il risultato di ogni combinazione unica di parametri in una mappa. Se il metodo viene richiamato con la stessa combinazione di parametri, il risultato viene recuperato dalla cache invece di essere ricalcolato.

Anche in questo caso applichiamo il decoratore ad una classe:

1class ExpensiveOperations {
2  @CacheResult
3  calculateExpensiveValue( a: number, b: number ): number {
4    console.log( "Esecuzione di un'operazione costosa..." );
5    return a * b; // Simulazione di un calcolo costoso
6  }
7}
8
9const expensiveOps = new ExpensiveOperations();
10console.log( expensiveOps.calculateExpensiveValue( 5, 10 ) );
11console.log( expensiveOps.calculateExpensiveValue( 5, 10 ) ); // Questa volta verrà usato il risultato dalla cache

In questa classe ExpensiveOperations, abbiamo un metodo calculateExpensiveValue. Questo metodo è decorato con @CacheResult. Quando viene chiamato per la prima volta con una coppia di parametri, il risultato viene calcolato e memorizzato. Se lo stesso metodo viene chiamato nuovamente con la stessa coppia di parametri, il risultato viene recuperato dalla cache.

Questo esempio illustra come i decoratori possano essere utilizzati in TypeScript per migliorare le prestazioni memorizzando i risultati delle operazioni e riducendo così la necessità di eseguire calcoli costosi più volte.

Conclusione

I decoratori in TypeScript offrono un modo potente e flessibile per aggiungere funzionalità aggiuntive alle classi e ai loro membri. Sebbene la loro natura sperimentale possa portare ad alcune limitazioni, il loro utilizzo può significativamente migliorare la leggibilità, la manutenibilità e le funzionalità del codice. Come ogni strumento, è importante utilizzarli con saggezza, considerando sempre il contesto specifico e le esigenze del progetto.

Caricamento...

Diventiamo amici di penna? ✒️

Iscriviti alla newsletter per ricevere una mail ogni paio di settimane con le ultime novità, gli ultimi approfondimenti e gli ultimi corsi gratuiti puubblicati. Ogni tanto potrei scrivere qualcosa di commerciale, ma solo se mi autorizzi, altrimenti non ti disturberò oltre.

Se non ti piace la newsletter ti ricordo la community su Discord, dove puoi chiedere aiuto, fare domande e condividere le tue esperienze (ma soprattutto scambiare due meme con me). Ti aspetto!

Ho in previsione di mandarti una newsletter ogni due settimane e una commerciale quando capita.