In questo articolo esploriamo una delle particolarità più interessanti di TypeScript: i decoratori! Si tratta di particolari costrutti che, se usate Angular o NestJS, conoscerete già alla perfezione: ci consentono di annotare una classe/metodo/proprietà/parametro per aggiungere dei metadati o modificarne il comportamento.

Ma sareste in grado di scrivere un decoratore da zero? Vediamo insieme come fare.

Tipologie di decoratori

Come dicevamo, ci sono 5 tipi di decoratori possibili in TypeScript:

  • ClassDecorator, per le classi
  • MethodDecorator, per i metodi
  • PropertyDecorator, per le proprietà
  • ParameterDecorator, per i parametri dei costruttori
  • AccessorDecorator, per getter/setter

Ecco invece a cosa dovete stare attenti:

  • Non è possibile applicare un decoratore a un costruttore (per quello esiste il ClassDecorator!)
  • Possiamo applicare più decoratori in cascata allo stesso elemento
  • Nel vostro tsconfig.json dovete aver impostato compilerOptions.experimentalDecorators a true!

Ma cos’è un decoratore? Un decoratore è semplicemente una funzione! Una funzione particolare però, che accetta dei parametri molto specifici che variano in base al tipo di decoratore.

Decoratori di classe

Parametri disponibili:

  • target: il costruttore della classe

Quando utilizziamo un decoratore su una classe, abbiamo un parametro a disposizione: il target, che è il costruttore della classe. Dobbiamo fare attenzione a una cosa: quando utilizziamo un decoratore di classe, questo verrà invocato quando la classe viene dichiarata, non quando creiamo nuove istanze.

Possiamo utilizzarlo per fare delle modifiche alla classe (anche pesanti, quindi attenzione a non fare disastri!), ad esempio possiamo freezarla:

// Applichiamo il decoratore con il simbolo @
@Freeze
class MyClass {}

// Rinominiamo "target" in "constructor" per chiarezza
function Freeze(constructor: Function) {
  // Freeziamo sia il costruttore che il tuo prototype
  Object.freeze(constructor);
  Object.freeze(constructor.prototype);
}

Possiamo anche utilizzarlo per agganciare dei metadati alla classe (un po’ come fa Angular), ad esempio con la libreria reflect-metadata, ma per farlo, questi metadati dobbiamo passarli in qualche modo: creiamo quindi una factory!

DecoratorFactory

import 'reflect-metadata';

@Meta('Questi sono i metadati')
class MyClass {}

function Meta(value: string) {
  // Ritorniamo il nostro decoratore!
  return function(constructor: Function) {
      Reflect.defineMetadata(“Meta”, value, constructor);
  }
}

// Testiamo
const myClass = new MyClass();
const value = Reflect.getMetadata(“Meta”, myClass.constructor);
console.log(value); // Stamperà 'Questi sono i metadati'

Decoratori di proprietà

Parametri disponibili:

  • target: il prototype della classe
  • propertyKey: il nome della proprietà

Il decoratore di proprietà è molto simile a quello di classe, ma target questa volta sarà il prototype della classe, e avremo a disposizione anche la propertyKey!

Scriviamo un decoratore che intercetti le modifiche alla nostra proprietà e che ci appenda una emoji sorridente!

function Smile() {
  return function(target: Object, propertyKey: string | symbol) {

    let val = target[propertyKey];

    const getter = () =>  {
        return val;
    };

    const setter = (next) => {
        val = `${next} :)`;
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });

  };
}

class Person {
  @Smile() greeting: string;
}

const Michele = new Person();
Michele.greeting = 'Ciao!';
console.log(Michele.greeting); // 'Ciao! :)'

Abbiamo utilizzato anche qui una DecoratorFactory pur non essendocene bisogno, perché? Perché è una buona prassi. Se domani volessimo aggiungere un parametro opzionale, non romperemmo il codice.

Decoratori di metodo

Parametri disponibili:

  • target: il prototype della classe (o il costruttore se il metodo è statico)
  • propertyKey: il nome del metodo
  • propertyDescriptor: un oggetto che descrive il metodo (qui trovate maggiori info, qui lo trovate tipizzato), se invece il metodo è statico, descrive il costruttore della classe

Scriviamo questa volta un decoratore per proteggere un metodo: se l’utente ci darà conferma, il metodo verrà chiamato, altrimenti non verrà fatto nulla.

export class Smartphone {

  @Confirm('Sicuro?')
  shutdown() {
    console.log('Sto spegnendo il telefono...');
  }
}


function Confirm(message: string) {
  return function (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) {
      // Salviamo il nostro metodo...
      const originalMethod = descriptor.value;

      // ...perché lo vogliamo sovrascrivere!
      descriptor.value = function() {
          // Chiediamo conferma all'utente
          const allow = confirm(message);

          // Se ha confermato, chiamiamo il metodo, altrimenti nulla.
          return allow ? original() : null;
    };

    // Ritorniamo il metodo modificato
    return descriptor;
  };
}

Decoratori di Accessor

Un decoratore di accessor (getter/setter) ha gli stessi parametri di un decoratore di metodo, che abbiamo appena visto.

C’è una cosa a cui stare attenti però: non è possibile decorare sia il getter che il setter di una stessa proprietà, perché il riferimento alla proprietà è lo stesso. Quindi, decorate il primo dei due che appare in ordine di scrittura.

Decoratori di parametro

Parametri disponibili:

  • target: il prototype della classe (attenzione: qui va usato any, non Function)
  • propertyKey: il nome del metodo
  • parameterIndex: l’indice del parametro fra le parentesi tonde del metodo

Ecco un esempio semplice semplice in cui utilizziamo il decoratore di parametro: Angular li utilizza per dare istruzioni al motore di Dependency Injection, ma noi ci accontentiamo di molto meno! 😛

class Person {
    greeting(@Log() language: string) {}
}

function Log() {
  return function(target: any, propertyKey: string | symbol, parameterIndex: number) {
	  console.log(target);         // Person prototype
	  console.log(propertyKey);    // 'greeting'
	  console.log(parameterIndex); // 0
  }
}

Anche qui, potete sbizzarrirvi come negli esempi precedenti.

Conclusione e raccomandazioni

Creare dei decoratori custom non è una pratica comune fra gli utilizzatori di framework o librerie: questo perché gli autori ci hanno già avvantaggiato creandoli per noi! Ma, se pensate di poter estrapolare della logica e che quella logica starebbe bene in un decoratore… fatelo! I decoratori sono un pattern bellissimo che ci permette di scrivere codice altamente espressivo. Tuttavia, quando manipoliamo le nostre classi a questo livello, dobbiamo stare attentissimo: il rischio di combinare disastri è alto, quindi assicuratevi di testare il vostro codice per tutti i possibili casi d’uso!

Ricordate, infine, che è possibile applicare decoratori in cascata allo stesso elemento, quindi cercate di dividere la logica in più decoratori se vedete che il decoratore che state scrivendo si sta occupando di troppe cose.

Ti è piaciuto l’articolo? Condividilo!

Rilasciare risorse gratuite non è facile: un sacco di tempo vola fra la ricerca dell’idea, la stesura, la preparazione di codice funzionante e la revisione. Se ti piace ciò che facciamo, condividi l’articolo: nel peggiore dei casi ti daranno del secchione!

Ti interesserà anche…

Iscriviti alla nostra newsletter!

Rimani aggiornato per quando rilasceremo nuovi articoli, nuovi corsinuove promozioni e nuove risorse gratuite. Ti promettiamo che useremo la tua email con prudenza, e non la condivideremo con nessuno senza il tuo consenso!

Abbiamo a cuore la tua privacy. Niente spam. Leggi la nostra privacy policy qui.

Menu