Ein Decorator ist eine Funktion, die du mit @name an eine Klasse oder einen Klassenmember hängst. Sie bekommt die ursprüngliche Methode (oder Klasse, oder das Feld) plus ein Kontextobjekt, das sie beschreibt, und kann einen Ersatz zurückgeben. TypeScript unterstützt die Standard-Decorators ohne Compiler-Flag.
@logged läuft einmal, wenn die Klasse definiert wird, und ersetzt add durch den zurückgegebenen Wrapper. Jeder Aufruf geht durch den Wrapper. Die generischen Parameter erhalten this, Argument und Rückgabetypen der Methode, also nimmt add weiterhin zwei Zahlen und gibt eine Zahl zurück.
Wie Decorators kompiliert werden
Standard-Decorators stammen aus einem TC39-Vorschlag für JavaScript, und TypeScript implementiert sie seit TypeScript 5.0. Der Vorschlag ist noch nicht Teil des JavaScript-Standards, und Node 24 parst die @-Syntax nicht. Ist das Target ES2022 (wie auf diesen Seiten), schreibt der Compiler daher jede dekorierte Klasse in reines JavaScript um, das Hilfsfunktionen aufruft (__esDecorate und __runInitializers, oben in der Datei ausgegeben). Die Ausgabe läuft überall, wo ES2022 läuft.
Eine Folge: Eine Datei mit Decorators läuft nicht mit dem eingebauten Type Stripping von Node (node file.ts), das nur Typen entfernt und das @ stehen lässt. Node bricht mit SyntaxError: Invalid or unexpected token ab. Kompiliere sie vorher mit tsc oder einem Bundler.
Arten von Decorators und ihre Signaturen
Jeder Standard-Decorator hat die Form (value, context) => replacement | void. Was value ist und was du zurückgeben darfst, hängt davon ab, was dekoriert wird:
| Dekoriert | value | Kontexttyp | Rückgabe |
|---|---|---|---|
| Klasse | die Klasse | ClassDecoratorContext | eine Ersatzklasse oder nichts |
| Methode | die Methode | ClassMethodDecoratorContext | eine Ersatzmethode |
| Getter / Setter | der Getter oder Setter | ClassGetterDecoratorContext / ClassSetterDecoratorContext | ein Ersatz-Getter oder Setter |
| Feld | undefined | ClassFieldDecoratorContext | eine Funktion, die den Startwert umwandelt |
accessor-Feld | { get, set } | ClassAccessorDecoratorContext | { get?, set?, init? } |
Jedes Kontextobjekt hat kind, name und addInitializer. Der Kontext eines Klassenmembers hat außerdem static, private und ein Objekt access, um den Member von einer Instanz zu lesen. Decorators funktionieren auch bei statischen und #private Membern.
Decorator Factories
Um Optionen zu übergeben, schreibst du eine Funktion, die einen Decorator zurückgibt, und rufst sie an der @-Stelle auf. Das ist eine Decorator Factory:
@retry(3) ruft zuerst retry auf, und die zurückgegebene Funktion ist der eigentliche Decorator. Mehrere Decorators stapeln sich: Bei @a @b method() wird b zuerst angewendet, und a umhüllt das Ergebnis.
Klassen-Decorators und addInitializer
Ein Klassen-Decorator bekommt die Klasse selbst. Er kann eine Unterklasse als Ersatz zurückgeben oder nichts zurückgeben und sie nur irgendwo registrieren. context.addInitializer registriert Code, der zu einem bestimmten Zeitpunkt läuft: bei einem Klassen-Decorator direkt nachdem die Klasse vollständig definiert ist, bei einem Methoden-Decorator beim Erzeugen jeder Instanz.
Der Klassen-Decorator steht über der Klasse (oder nach export). Ohne @bound würde der Aufruf von loose() werfen, weil this undefined wäre.
Feld und Accessor-Decorators
Ein Feld-Decorator kann spätere Zuweisungen weder sehen noch abfangen: Sein value ist undefined, und er kann nur eine Funktion zurückgeben, die den Startwert des Felds umwandelt. Um Lese und Schreibzugriffe abzufangen, deklarierst du das Feld mit dem Schlüsselwort accessor, das es in ein Getter-Setter-Paar mit privatem Speicher verwandelt, und dekorierst das:
accessor gehört zum selben Vorschlag. Es erzeugt einen echten Getter und Setter über einem #private Feld, und deshalb geht p.price = -5 durch das set des Decorators.
Standard vs Legacy experimentalDecorators
Vor TypeScript 5.0 hatte TypeScript nur eine frühe Version des Vorschlags, aktiviert mit experimentalDecorators. Dieses Flag gibt es weiterhin, und es schaltet den Compiler auf das Legacy-Modell mit anderen Signaturen und anderer Semantik um:
| Standard (kein Flag) | Legacy (experimentalDecorators) | |
|---|---|---|
| Signatur | (value, context) | (target, propertyKey, descriptor) |
| Wie eine Methode geändert wird | gibt eine neue Funktion zurück | verändert descriptor.value |
| Parameter-Decorators | nicht unterstützt (TS1206) | unterstützt |
emitDecoratorMetadata | nicht unterstützt | unterstützt (Typinformationen zur Laufzeit über reflect-metadata) |
accessor-Decorators ({ get, set, init }), addInitializer | ja | nein |
| Grundlage | der TC39-Vorschlag | ein älterer Entwurf davon |
Ein für ein Modell geschriebener Decorator besteht die Typprüfung im anderen nicht. Hier ein Decorator im Legacy-Stil in einem Projekt ohne das 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.
Die Lösung ist, ihn entweder in die Standardform (value, context) umzuschreiben, wie im ersten Beispiel dieser Seite, oder das Legacy-Modell für das ganze Projekt einzuschalten:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Angular, NestJS und TypeORM setzen experimentalDecorators weiterhin in den Konfigurationen, die ihre Tools erzeugen und die ihre Dokumentation verlangt. NestJS und TypeORM brauchen außerdem emitDecoratorMetadata, weil sie Typen zur Laufzeit lesen: NestJS, um Konstruktorparameter zu injizieren, TypeORM, um Eigenschaften auf Spalten abzubilden:
// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
constructor(@Inject(DB) private db: Database) {}
}
Wenn du eines dieser Frameworks nutzt, schreibe Decorators im Legacy-Stil und folge seiner Dokumentation. Für neuen Code ohne ein solches Framework nimm die Standard-Decorators.
Wann Decorators sinnvoll sind
Decorators passen zu Querschnittsverhalten, das sonst in vielen Methoden wiederholt würde: Logging, Zeitmessung, Caching, Wiederholungen, Zugriffsprüfungen, Validierung und das Registrieren von Klassen bei einem Container oder Router. Sie verbergen aber auch den Kontrollfluss, denn wer den Code liest, muss erst nachschlagen, was @retry tut, bevor er weiß, was die Methode tut. Für ein oder zwei Einsätze ist eine einfache Higher-Order Function (const fetchData = retry(3, rawFetch)) einfacher und funktioniert auch außerhalb von Klassen.
Häufig gestellte Fragen
Was sind Decorators in TypeScript?
Ein Decorator ist eine Funktion, die mit der Syntax @name auf eine Klasse oder einen Klassenmember angewendet wird. Sie bekommt das dekorierte Element und ein Kontextobjekt und kann einen Ersatz zurückgeben: eine umhüllte Methode, eine neue Klasse oder eine Funktion, die den Startwert eines Felds umwandelt. Übliche Einsätze sind Logging, Validierung, Caching und das Registrieren von Klassen.
Brauche ich experimentalDecorators, um in TypeScript Decorators zu verwenden?
Nein. Seit TypeScript 5.0 funktionieren die Standard-Decorators (TC39) ohne Flag. experimentalDecorators schaltet den Compiler auf das ältere Legacy-Modell um, auf dem Frameworks wie Angular und NestJS aufbauen. Die beiden verwenden unterschiedliche Funktionssignaturen und sind nicht austauschbar.
Was ist der Unterschied zwischen Standard-Decorators und experimentalDecorators?
Standard-Decorators bekommen (value, context) und geben einen Ersatz zurück. Legacy-Decorators bekommen (target, propertyKey, descriptor) und verändern den Property Descriptor. Nur das Legacy-Modell unterstützt Parameter-Decorators und emitDecoratorMetadata; nur das Standardmodell hat accessor-Decorators, die { get, set, init } zurückgeben, und context.addInitializer.
Unterstützt TypeScript Parameter-Decorators?
Nur mit aktivem experimentalDecorators. Im Standardmodus ist ein Decorator auf einem Parameter der Fehler TS1206: Decorators are not valid here, weil der TC39-Vorschlag keine Parameter-Decorators enthält. Frameworks für Dependency Injection, die Konstruktorparameter dekorieren, brauchen daher das Legacy-Flag.
In welcher Reihenfolge werden mehrere Decorators angewendet?
Die Decorator-Ausdrücke werden von oben nach unten ausgewertet, aber von unten nach oben angewendet: Bei @a @b method() umhüllt b die Methode zuerst, und a umhüllt das Ergebnis. Beim Aufruf der Methode läuft a also außen.