Von TC39 Stage 3 bis zur Dependency Injection in Angular und NestJS
Decorators erweitern Klassen, Methoden und Properties um wiederverwendbares Verhalten, ohne den eigentlichen Code zu verändern. Seit TypeScript 5.0 steht dafür der neue TC39 Stage 3 Standard ohne Compiler-Flag bereit, während Angular und ältere NestJS-Versionen weiterhin auf experimentalDecorators und reflect-metadata setzen. Dieser Artikel zeigt beide Systeme mit echten Codebeispielen für Logging, Validierung und Dependency Injection.
Inhaltsverzeichnis
- 1. Was Decorators wirklich sind und wann sie ausgeführt werden
- 2. TC39 Stage 3 Decorators versus das Legacy-System
- 3. Syntaxunterschiede: Context-API versus target und descriptor
- 4. Praxisbeispiel: Ein @log Method-Decorator mit der neuen Context-API
- 5. Property-Decorator: @readonly und Zugriffskontrolle
- 6. Decorator-Factories: Parametrisierte Decorators wie @validate(schema)
- 7. Dependency Injection: Wie Angular und NestJS Decorators nutzen
- 8. Vorsicht bei Overuse: Debugging, Stacktraces und Lesbarkeit
- 9. Migration und Vergleich: Legacy Decorators versus TC39 Stage 3
- 10. Zusammenfassung
- 11. FAQ
1. Was Decorators wirklich sind und wann sie ausgeführt werden
Ein Decorator ist syntaktisch eine Funktion, die mit dem @-Symbol vor einer Klasse, Methode, einem Property oder Accessor notiert wird und beim Laden des Moduls einmalig ausgeführt wird, nicht bei jeder Instanziierung eines Objekts. Diese Ausführungszeit ist der zentrale Unterschied zu regulären Methodenaufrufen: Ein Decorator läuft, sobald die Klasse definiert wird, und kann die Deklaration selbst ersetzen, umschließen oder mit zusätzlichem Verhalten versehen, bevor überhaupt ein Objekt erzeugt wird.
Praktisch bedeutet das: Ein @log-Decorator auf einer Methode wickelt die ursprüngliche Funktion in eine neue Funktion ein, die vor und nach dem eigentlichen Aufruf zusätzlichen Code ausführt, etwa ein Log-Statement mit den übergebenen Argumenten. Da diese Ersetzung einmalig zur Definitionszeit stattfindet, entsteht kein Laufzeit-Overhead pro Aufruf, der über den des Wrappers selbst hinausgeht. Diese Eigenschaft macht Decorators ideal für Querschnittsbelange wie Logging, Caching, Validierung oder Zugriffskontrolle, die sich sonst nur durch wiederholten Boilerplate-Code in jeder einzelnen Methode umsetzen ließen.
2. TC39 Stage 3 Decorators versus das Legacy-System
Bis TypeScript 4.x gab es nur eine einzige Decorator-Implementierung, aktiviert über das Compiler-Flag experimentalDecorators, die auf einem sehr frühen TC39-Vorschlag basierte und sich von der finalen ECMAScript-Spezifikation unterschied. Mit TypeScript 5.0 wurde der TC39 Stage 3 Vorschlag für Decorators nativ implementiert, ohne dass irgendein Compiler-Flag gesetzt werden muss. Dieser neue Standard orientiert sich an der tatsächlichen JavaScript-Spezifikation und wird künftig auch ohne TypeScript in nativen JavaScript-Engines lauffähig sein, sobald Browser und Node.js ihn vollständig unterstützen.
Trotzdem ist das Legacy-System keineswegs verschwunden: Angular basiert bis heute auf experimentalDecorators in Kombination mit emitDecoratorMetadata und der Bibliothek reflect-metadata, um zur Laufzeit Typinformationen für die Dependency Injection auszulesen. Auch ältere NestJS-Projekte, die vor einer Migration auf die neuen Decorators stehen, benötigen diese Kombination weiterhin. Wer ein neues Projekt startet, sollte grundsätzlich beim modernen Standard ohne Flag bleiben, wer ein bestehendes Angular- oder NestJS-Projekt pflegt, muss die Legacy-Konfiguration vorerst beibehalten, bis die Frameworks selbst vollständig migrieren.
{
"compilerOptions": {
// Legacy system: required for Angular and older NestJS versions
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// Modern TC39 Stage 3 decorators need neither flag above.
// Simply omit both options and use TypeScript 5.0 or newer,
// the compiler applies the standard decorator semantics by default.
"target": "ES2022",
"module": "NodeNext",
"strict": true
}
}
3. Syntaxunterschiede: Context-API versus target und descriptor
Ein Legacy-Methodendecorator erhält drei Parameter: target (den Prototyp oder Konstruktor), propertyKey (den Methodennamen als String oder Symbol) und descriptor (das PropertyDescriptor-Objekt mit value, writable, enumerable und configurable). Um Verhalten zu ändern, muss der Decorator descriptor.value manuell überschreiben und das veränderte descriptor-Objekt zurückgeben oder undefined, wenn descriptor.value direkt mutiert wurde. Diese Signatur ist lose typisiert, da target je nach Deklarationsart unterschiedliche Formen annehmen kann und TypeScript hier kaum Typsicherheit bieten kann.
Die neuen TC39 Stage 3 Decorators erhalten stattdessen zwei Parameter: den zu dekorierenden Wert selbst, etwa die Methode als Funktion, und ein ClassMethodDecoratorContext-Objekt mit klar typisierten Feldern wie kind, name, static, private und der Methode addInitializer für Code, der erst beim Erzeugen einer Instanz laufen soll. Der Decorator gibt entweder nichts zurück oder eine neue Funktion, die den ursprünglichen Wert ersetzt. Diese Signatur ist vollständig typisiert und für jede Decorator-Art, Klasse, Methode, Feld, Accessor, eigenständig im TypeScript-Compiler definiert.
4. Praxisbeispiel: Ein @log Method-Decorator mit der neuen Context-API
Ein @log-Decorator demonstriert das neue System anschaulich: Er nimmt die Originalmethode entgegen, erzeugt eine ersetzende Funktion, die Argumente und Rückgabewert protokolliert, und gibt diese Funktion zurück. Der context-Parameter liefert dabei den Methodennamen für die Log-Ausgabe, ohne dass propertyKey manuell in einen String konvertiert werden müsste. Da TypeScript den Rückgabewert des Decorators exakt gegen die Originalsignatur prüft, schlägt ein Tippfehler in der Wrapper-Funktion schon beim Kompilieren fehl, statt erst zur Laufzeit einen kryptischen Fehler zu erzeugen.
Für Fälle, in denen ein Decorator zusätzlichen Zustand pro Instanz benötigt, etwa einen Zähler für Aufrufe, bietet context.addInitializer eine Callback-Funktion, die im Konstruktor jeder neuen Instanz ausgeführt wird und dabei auf this zugreifen kann. Das ersetzt den Umweg über WeakMaps, den Legacy-Decorators häufig brauchten, um Instanzdaten außerhalb der eigentlichen Klasse zu verwalten. In der Praxis reduziert das den Code für zustandsbehaftete Decorators erheblich und macht ihn deutlich einfacher nachzuvollziehen.
// Modern TC39 Stage 3 method decorator, no compiler flag needed
function log<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log(`[LOG] Calling ${methodName} with`, args);
const result = target.call(this, ...args);
console.log(`[LOG] ${methodName} returned`, result);
return result;
}
return replacementMethod;
}
class OrderService {
@log
placeOrder(orderId: string, quantity: number): string {
return `Order ${orderId} placed with quantity ${quantity}`;
}
}
const service = new OrderService();
service.placeOrder("A-1001", 3);
// [LOG] Calling placeOrder with [ 'A-1001', 3 ]
// [LOG] placeOrder returned Order A-1001 placed with quantity 3
5. Property-Decorator: @readonly und Zugriffskontrolle
Property-Decorators im neuen System unterscheiden sich grundlegend von Methoden-Decorators, weil einfache Klassenfelder keinen Property-Descriptor mit einem austauschbaren value besitzen. Für Felder, deren Zugriff abgefangen werden soll, wird deshalb das Schlüsselwort accessor verwendet, das TypeScript automatisch einen versteckten Getter und Setter generieren lässt. Ein @readonly-Decorator auf einem solchen Auto-Accessor kann den generierten Setter durch eine Funktion ersetzen, die bei jedem Schreibversuch nach der Initialisierung eine Exception wirft.
Reine Klassenfelder ohne accessor-Schlüsselwort erhalten stattdessen eine initializer-Funktion, die der Decorator umschließen kann, um den Startwert zu transformieren, etwa um Eingabedaten zu trimmen oder zu normalisieren, bevor sie im Feld landen. Legacy-Property-Decorators konnten dagegen nur sehr eingeschränkt in die Initialisierung eingreifen, weil sie ausschließlich Zugriff auf target und propertyKey hatten, aber keinen Bezug zum tatsächlichen Anfangswert. Diese Lücke schließt die neue Context-API vollständig und macht deklarative Validierung direkt am Feld praktikabel.
// Auto-accessor decorator: block writes after initialization
function readonly<This, Value>(
target: ClassAccessorDecoratorTarget<This, Value>,
context: ClassAccessorDecoratorContext<This, Value>
): ClassAccessorDecoratorResult<This, Value> {
return {
get(this: This): Value {
return target.get.call(this);
},
set(this: This, newValue: Value): void {
throw new Error(`Cannot assign to read only property ${String(context.name)}`);
},
init(this: This, initialValue: Value): Value {
return initialValue;
},
};
}
class ApiConfig {
@readonly
accessor baseUrl: string = "https://api.mironsoft.de/v1";
}
const config = new ApiConfig();
console.log(config.baseUrl); // https://api.mironsoft.de/v1
config.baseUrl = "https://evil.example.com"; // throws Error
6. Decorator-Factories: Parametrisierte Decorators wie @validate(schema)
Ein einfacher Decorator wie @log nimmt keine eigenen Argumente entgegen, aber viele reale Anwendungsfälle brauchen Konfiguration, etwa ein Validierungsschema oder ein Cache-Zeitlimit. Die Lösung ist eine Decorator-Factory: eine gewöhnliche Funktion, die die gewünschten Argumente entgegennimmt und selbst einen Decorator zurückgibt. Diese zusätzliche Funktionsebene ändert nichts an der grundlegenden Mechanik, jeder zurückgegebene Decorator folgt weiterhin exakt der Context-API-Signatur, erlaubt aber, denselben Decorator-Typ mit unterschiedlicher Konfiguration mehrfach im selben Projekt zu verwenden.
Ein @validate(schema)-Decorator prüft beispielsweise vor jedem Methodenaufruf die übergebenen Argumente gegen ein Schema, etwa mit Zod oder einer eigenen Validierungsfunktion, und wirft bei Verstößen eine aussagekräftige Fehlermeldung, bevor der eigentliche Methodencode überhaupt läuft. Dieses Pattern verhindert, dass Validierungslogik über viele Methoden verstreut und dupliziert wird, und macht Validierungsregeln an einer einzigen, gut sichtbaren Stelle direkt über der Methodensignatur lesbar, statt sie irgendwo im Methodenrumpf zu verstecken.
// Decorator factory: a function that returns a configured decorator
interface Schema<T> {
parse(value: unknown): T;
}
function validate<This, Args extends unknown[], Return>(schema: Schema<Args[0]>) {
return function (
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
return function (this: This, ...args: Args): Return {
schema.parse(args[0]);
return target.call(this, ...args);
};
};
}
const orderSchema: Schema<{ quantity: number }> = {
parse(value) {
const input = value as { quantity: number };
if (input.quantity <= 0) {
throw new Error("quantity must be greater than zero");
}
return input;
},
};
class Checkout {
@validate(orderSchema)
submit(order: { quantity: number }): void {
console.log("Order submitted", order);
}
}
7. Dependency Injection: Wie Angular und NestJS Decorators nutzen
Angular und NestJS nutzen Decorators nicht nur zur Codeorganisation, sondern als tragendes Fundament ihrer Dependency-Injection-Container. @Injectable markiert eine Klasse als vom DI-Container verwaltbar, @Component beziehungsweise @Controller registriert Metadaten über Selektoren oder Routen, und Konstruktor-Parameter-Decorators wie @Inject markieren, welche Abhängigkeit an welcher Position injiziert werden soll. All diese Informationen werden zur Laufzeit über reflect-metadata gespeichert und ausgelesen, sobald der Container eine Instanz der Klasse erzeugen muss.
Genau diese Laufzeit-Reflexion ist der Grund, warum Angular und ältere NestJS-Versionen weiterhin experimentalDecorators mit emitDecoratorMetadata benötigen: Nur damit emittiert der TypeScript-Compiler design:paramtypes-Metadaten, aus denen der Container die Konstruktortypen zur Auflösung der Abhängigkeiten ableiten kann. Die neuen TC39 Stage 3 Decorators unterstützen bislang keine Parameter-Decorators und keine automatische Typmetadaten-Emission in gleicher Form, weshalb eine vollständige Migration dieser Frameworks auf den neuen Standard technisch aufwendig ist und noch nicht flächendeckend erfolgt ist.
// NestJS-style legacy decorators, backed by reflect-metadata
import "reflect-metadata";
@Injectable()
class ProductService {
findAll(): string[] {
return ["Product A", "Product B"];
}
}
@Controller("products")
class ProductController {
// Parameter decorator marks the constructor dependency for the DI container
constructor(private readonly productService: ProductService) {}
@Get()
list(): string[] {
return this.productService.findAll();
}
}
// At bootstrap time, Nest reads design:paramtypes metadata emitted by
// emitDecoratorMetadata to resolve ProductService and inject the instance.
8. Vorsicht bei Overuse: Debugging, Stacktraces und Lesbarkeit
Jeder zusätzliche Decorator fügt eine weitere Indirektionsebene zwischen Aufruf und tatsächlicher Ausführung ein. Bei einer einzelnen Methode mit einem @log-Decorator ist das unproblematisch, aber Klassen mit fünf oder mehr gestapelten Decorators, die sich teilweise gegenseitig in ihrer Reihenfolge beeinflussen, werden schnell schwer nachvollziehbar. Stacktraces bei Fehlern enthalten dann zusätzliche Wrapper-Frames, die den eigentlichen Fehlerursprung verschleiern, und ein Debugger muss sich durch mehrere generierte Zwischenfunktionen hangeln, bevor er den eigentlichen Methodencode erreicht.
Die Ausführungsreihenfolge gestapelter Decorators folgt zudem einer nicht immer intuitiven Regel: Decorator-Ausdrücke werden von oben nach unten ausgewertet, aber die resultierenden Funktionen von unten nach oben angewendet, ähnlich wie bei verschachtelten Funktionsaufrufen. Teams sollten Decorators deshalb bewusst für klar abgegrenzte Querschnittsbelange einsetzen, etwa Logging oder Validierung, und nicht als generisches Werkzeug für beliebige Geschäftslogik missbrauchen. Ein Decorator, dessen Effekt sich nicht in einem Satz erklären lässt, ist meist ein Kandidat für eine gewöhnliche, explizit aufgerufene Funktion.
9. Migration und Vergleich: Legacy Decorators versus TC39 Stage 3
Teams, die von experimentalDecorators auf den neuen Standard migrieren wollen, sollten zunächst prüfen, ob alle verwendeten Bibliotheken bereits kompatibel sind. Frameworks, die auf Parameter-Decorators und reflect-metadata angewiesen sind, wie Angular oder NestJS, lassen sich derzeit nicht ohne Weiteres auf die neuen Decorators umstellen, während eigene Utility-Decorators wie @log, @readonly oder @validate meist unkompliziert übertragbar sind. Die folgende Tabelle vergleicht beide Systeme entlang der wichtigsten Entscheidungsdimensionen für ein Migrationsprojekt.
| Aspekt | Legacy: experimentalDecorators | Modern: TC39 Stage 3 |
|---|---|---|
| Compiler-Flag | experimentalDecorators: true zwingend nötig | Kein Flag, seit TypeScript 5.0 Standardverhalten |
| Metadaten-Reflexion | emitDecoratorMetadata + reflect-metadata Paket nötig | Context-Objekt liefert Metadaten nativ, kein externes Paket |
| Framework-Unterstützung | Erforderlich für Angular und ältere NestJS-Versionen | Wachsend, DI-Frameworks migrieren erst schrittweise |
| Typsicherheit der Signatur | target: any, lose typisiert | Vollständig typisiert über ClassMethodDecoratorContext & Co. |
| Standardisierungsstatus | Proprietärer TypeScript-Vorschlag, nie standardisiert | TC39 Stage 3, Teil der zukünftigen ECMAScript-Spezifikation |
In der Praxis bedeutet das: Neue Projekte ohne Angular- oder NestJS-Abhängigkeit sollten von Anfang an auf die TC39 Stage 3 Decorators ohne Compiler-Flag setzen. Bestehende Angular- oder NestJS-Codebasen bleiben vorerst auf dem Legacy-System, bis die jeweiligen Frameworks selbst offiziell migrieren, was für beide Ökosysteme bereits angekündigt, aber noch nicht abgeschlossen ist. Eine gemischte Nutzung beider Systeme im selben tsconfig ist technisch nicht vorgesehen, weshalb die Entscheidung projektweit einheitlich getroffen werden muss.
Mironsoft
TypeScript-Tooling, Build-Skripte und Headless-Integrationen für Magento und Hyvä
TypeScript-Code, der wartbar bleibt statt zu wuchern?
Wir analysieren bestehende TypeScript-Codebasen, migrieren von experimentalDecorators auf den TC39 Stage 3 Standard und bauen saubere, typsichere Decorator-Patterns für Logging, Validierung und Dependency Injection in eurem Stack.
Code-Review
Analyse bestehender Decorator-Patterns und tsconfig-Konfiguration
Migration
Schrittweiser Umstieg von experimentalDecorators auf TC39 Stage 3
Headless-Tooling
TypeScript-Build-Skripte und API-Clients für Magento-Integrationen
10. Zusammenfassung
Decorators in TypeScript lösen ein wiederkehrendes Problem eleganter als klassische Vererbung oder manuelles Wrapping: Sie fügen Klassen, Methoden, Properties und Accessorn zur Definitionszeit zusätzliches Verhalten hinzu, ohne den eigentlichen Code zu verändern. Seit TypeScript 5.0 ist der TC39 Stage 3 Standard ohne jedes Compiler-Flag verfügbar und löst das alte, proprietäre System nach und nach ab. Wer heute ein neues Projekt startet, sollte deshalb konsequent auf die moderne Context-API setzen, statt experimentalDecorators zu aktivieren.
Gleichzeitig bleibt das Legacy-System relevant, solange Angular und ältere NestJS-Versionen für ihre Dependency-Injection-Container auf reflect-metadata und Parameter-Decorators angewiesen sind. Praktische Anwendungsfälle wie Logging, Validierung, Zugriffskontrolle und Dependency Injection profitieren in beiden Systemen von derselben Grundidee: wiederkehrendes Verhalten an einer zentralen Stelle zu definieren, statt es in jeder einzelnen Methode zu duplizieren. Wer Decorators gezielt und sparsam einsetzt, gewinnt lesbaren, wartbaren Code, wer sie überstrapaziert, erkauft sich schwer debugbare Indirektionsebenen.
Decorators in TypeScript, Das Wichtigste auf einen Blick
Ausführungszeitpunkt
Decorators laufen bei der Klassendefinition, nicht bei der Instanziierung, deshalb kein Laufzeit-Overhead pro Objekt.
TC39 Stage 3 Standard
Seit TypeScript 5.0 ohne Compiler-Flag verfügbar, mit typisierter Context-API für Klassen, Methoden, Felder und Accessors.
Legacy weiterhin nötig für DI
Angular und ältere NestJS-Versionen benötigen experimentalDecorators, emitDecoratorMetadata und reflect-metadata.
Sparsam einsetzen
Gestapelte Decorators erschweren Debugging und Stacktraces, deshalb nur für klar abgegrenzte Querschnittsbelange nutzen.