Menu

Decorator in TypeScript: metodi, classi, campi e accessor

I decorator sono funzioni che avvolgono o sostituiscono i membri di una classe con la sintassi @. Scopri i decorator standard che TypeScript supporta senza alcun flag (classe, metodo, getter, campo e accessor), le decorator factory, addInitializer e in cosa differiscono dai vecchi experimentalDecorators usati da Angular e NestJS.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un decorator è una funzione che applichi a una classe o a un membro di una classe con @name. Riceve il metodo originale (o la classe, o il campo) più un oggetto di contesto che lo descrive, e può restituire un sostituto. TypeScript supporta i decorator standard senza alcun flag del compilatore.

@logged viene eseguito una volta, quando la classe viene definita, e sostituisce add con il wrapper che restituisce. Ogni chiamata passa dal wrapper. I parametri generici mantengono intatti i tipi di this, degli argomenti e del ritorno del metodo, quindi add accetta ancora due numeri e restituisce un numero.

Come vengono compilati i decorator

I decorator standard vengono da una proposta TC39 per JavaScript, e TypeScript li implementa da TypeScript 5.0. La proposta non fa ancora parte dello standard JavaScript, e Node 24 non riconosce la sintassi @, quindi quando il target è ES2022 (come in queste pagine) il compilatore riscrive ogni classe decorata in JavaScript semplice che chiama funzioni helper (__esDecorate e __runInitializers, emesse in cima al file). L'output funziona ovunque funzioni ES2022.

Una conseguenza: un file con decorator non può essere eseguito con il type stripping integrato di Node (node file.ts), che rimuove solo i tipi e lascia la @ al suo posto. Node si ferma con SyntaxError: Invalid or unexpected token. Compilalo prima con tsc o con un bundler.

Tipi di decorator e le loro firme

Ogni decorator standard ha la forma (value, context) => replacement | void. Cosa sia value, e cosa puoi restituire, dipende da ciò che viene decorato:

DecoravalueTipo del contestoRitorno
classela classeClassDecoratorContextuna classe sostitutiva, o niente
metodoil metodoClassMethodDecoratorContextun metodo sostitutivo
getter / setteril getter o setterClassGetterDecoratorContext / ClassSetterDecoratorContextun getter o setter sostitutivo
campoundefinedClassFieldDecoratorContextuna funzione che trasforma il valore iniziale
campo accessor{ get, set }ClassAccessorDecoratorContext{ get?, set?, init? }

Ogni oggetto di contesto ha kind, name e addInitializer. Il contesto di un membro di classe ha anche static, private e un oggetto access per leggere il membro da un'istanza. I decorator funzionano anche sui membri statici e #private.

Decorator factory

Per passare delle opzioni, scrivi una funzione che restituisce un decorator e chiamala nel punto della @. Questa è una decorator factory:

@retry(3) chiama prima retry, e la funzione che restituisce è il decorator vero e proprio. Più decorator si impilano: in @a @b method(), b viene applicato per primo e a avvolge il risultato.

Decorator di classe e addInitializer

Un decorator di classe riceve la classe stessa. Può restituire una sottoclasse per sostituirla, oppure non restituire nulla e limitarsi a registrarla da qualche parte. context.addInitializer registra del codice da eseguire in un momento preciso: per un decorator di classe, subito dopo che la classe è stata definita del tutto; per un decorator di metodo, quando ogni istanza viene costruita.

Il decorator di classe va sopra la classe (o dopo export). Senza @bound, chiamare loose() lancerebbe un errore, perché this sarebbe undefined.

Decorator di campo e di accessor

Un decorator di campo non può vedere né intercettare le assegnazioni successive: il suo value è undefined, e tutto ciò che può restituire è una funzione che trasforma il valore iniziale del campo. Per intercettare letture e scritture, dichiara il campo con la parola chiave accessor, che lo trasforma in una coppia getter e setter basata su uno spazio privato, e decora quello:

accessor fa parte della stessa proposta. Emette un vero getter e setter sopra un campo #private, ed è per questo che p.price = -5 passa dal set del decorator.

Standard vs experimentalDecorators legacy

Prima di TypeScript 5.0, gli unici decorator di TypeScript erano una versione iniziale della proposta, attivata con experimentalDecorators. Quel flag esiste ancora, e fa passare il compilatore al modello legacy, con firme e semantica diverse:

Standard (nessun flag)Legacy (experimentalDecorators)
Firma(value, context)(target, propertyKey, descriptor)
Come modifica un metodorestituisce una nuova funzionemodifica descriptor.value
Decorator di parametronon supportati (TS1206)supportati
emitDecoratorMetadatanon supportatosupportato (informazioni di tipo a runtime tramite reflect-metadata)
Decorator accessor ({ get, set, init }), addInitializersìno
Basato sula proposta TC39una sua bozza precedente

Un decorator scritto per un modello non supera il controllo dei tipi nell'altro. Ecco un decorator in stile legacy in un progetto senza il flag:

index.ts(11,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.

La soluzione è riscriverlo nella forma standard (value, context), come nel primo esempio di questa pagina, oppure attivare il modello legacy per tutto il progetto:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Angular, NestJS e TypeORM impostano ancora experimentalDecorators nelle configurazioni generate dai loro strumenti e richieste dalla loro documentazione. NestJS e TypeORM hanno bisogno anche di emitDecoratorMetadata, perché leggono i tipi a runtime: NestJS per iniettare i parametri del costruttore, TypeORM per mappare le proprietà sulle colonne:

// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
    constructor(@Inject(DB) private db: Database) {}
}

Se usi uno di questi framework, scrivi i decorator alla maniera legacy e segui la sua documentazione. Per il codice nuovo senza un framework del genere, usa i decorator standard.

Quando usare i decorator

I decorator sono adatti a comportamenti trasversali che altrimenti si ripeterebbero in molti metodi: logging, misurazione dei tempi, cache, tentativi ripetuti, controlli di accesso, validazione e registrazione di classi in un container o in un router. Però nascondono anche il flusso di controllo, perché chi legge deve andare a vedere cosa fa @retry prima di sapere cosa fa il metodo. Per uno o due usi, una semplice funzione di ordine superiore (const fetchData = retry(3, rawFetch)) è più semplice e funziona anche fuori dalle classi.

Domande frequenti

Cosa sono i decorator in TypeScript?

Un decorator è una funzione applicata a una classe o a un membro di una classe con la sintassi @name. Riceve l'elemento decorato e un oggetto di contesto, e può restituire un sostituto: un metodo avvolto, una nuova classe o una funzione che trasforma il valore iniziale di un campo. Gli usi comuni sono logging, validazione, cache e registrazione di classi.

Serve experimentalDecorators per usare i decorator in TypeScript?

No. Da TypeScript 5.0, i decorator standard (TC39) funzionano senza alcun flag. experimentalDecorators fa passare il compilatore al vecchio modello di decorator legacy, su cui si basano framework come Angular e NestJS. I due usano firme di funzione diverse e non sono intercambiabili.

Che differenza c'è tra i decorator standard e experimentalDecorators?

I decorator standard ricevono (value, context) e restituiscono un sostituto. I decorator legacy ricevono (target, propertyKey, descriptor) e modificano il property descriptor. Solo il modello legacy supporta i decorator di parametro e emitDecoratorMetadata; solo il modello standard ha i decorator accessor che restituiscono { get, set, init } e context.addInitializer.

TypeScript supporta i decorator di parametro?

Solo con experimentalDecorators attivo. In modalità standard un decorator su un parametro è l'errore TS1206: Decorators are not valid here, perché la proposta TC39 non include i decorator di parametro. I framework di dependency injection che decorano i parametri del costruttore hanno quindi bisogno del flag legacy.

In che ordine vengono applicati più decorator?

Le espressioni dei decorator vengono valutate dall'alto in basso, ma applicate dal basso in alto: in @a @b method(), b avvolge per primo il metodo e a avvolge il risultato. Quindi quando il metodo viene chiamato a è il più esterno.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA