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:
| Decora | value | Tipo del contesto | Ritorno |
|---|---|---|---|
| classe | la classe | ClassDecoratorContext | una classe sostitutiva, o niente |
| metodo | il metodo | ClassMethodDecoratorContext | un metodo sostitutivo |
| getter / setter | il getter o setter | ClassGetterDecoratorContext / ClassSetterDecoratorContext | un getter o setter sostitutivo |
| campo | undefined | ClassFieldDecoratorContext | una 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 metodo | restituisce una nuova funzione | modifica descriptor.value |
| Decorator di parametro | non supportati (TS1206) | supportati |
emitDecoratorMetadata | non supportato | supportato (informazioni di tipo a runtime tramite reflect-metadata) |
Decorator accessor ({ get, set, init }), addInitializer | sì | no |
| Basato su | la proposta TC39 | una 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.