Decorators in TypeScript: Grundlagen und Anwendungsfälle
<T>
type
TypeScript · Decorators · TC39 · Metaprogrammierung
Decorators in TypeScript: Grundlagen und Anwendungsfälle
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.

12 Min. Lesezeit TC39 Stage 3 · experimentalDecorators · reflect-metadata TypeScript 5 · Angular · NestJS

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.

11. FAQ: Decorators in TypeScript

1Was ist ein Decorator in TypeScript?
Eine Funktion mit @-Syntax vor Klasse, Methode, Property oder Accessor, die zur Definitionszeit zusätzliches Verhalten hinzufügt, ohne den ursprünglichen Code zu verändern.
2Wann genau wird ein Decorator ausgeführt?
Einmalig bei der Klassendefinition beim Laden des Moduls, nicht bei jeder Instanziierung eines Objekts.
3TC39 Stage 3 versus experimentalDecorators?
TC39 Stage 3 ist der finale Standard, seit TypeScript 5.0 ohne Flag. experimentalDecorators ist das ältere, nie standardisierte System.
4Brauche ich in TypeScript 5 noch experimentalDecorators?
Nur für Frameworks wie Angular oder ältere NestJS-Versionen mit Parameter-Decorator-basierter Dependency Injection.
5Warum nutzen Angular und NestJS die alten Decorators?
Ihre DI-Container lesen design:paramtypes-Metadaten aus, die nur emitDecoratorMetadata mit experimentalDecorators erzeugt.
6Was ist reflect-metadata und wofür wird es gebraucht?
Eine Bibliothek, die zur Laufzeit Typinformationen an Klassen anhängt und ausliest, Grundlage für DI-Container wie in Angular oder NestJS.
7Wie schreibe ich einen eigenen Decorator wie @log?
Der Decorator nimmt Originalmethode und Context entgegen und gibt eine Ersatzfunktion zurück, die zusätzlichen Code vor und nach dem Aufruf ausführt.
8Was ist eine Decorator-Factory?
Eine Funktion, die eigene Argumente entgegennimmt und selbst einen Decorator zurückgibt, etwa @validate(schema) mit unterschiedlicher Konfiguration.
9Können Decorators Argumente entgegennehmen?
Direkt nicht, nur über eine Decorator-Factory, die konfiguriert wird und den eigentlichen Decorator zurückgibt.
10Was sind die Risiken beim übermäßigen Einsatz von Decorators?
Erschwertes Debugging, längere Stacktraces durch zusätzliche Wrapper-Frames und eine nicht immer intuitive Ausführungsreihenfolge bei mehreren gestapelten Decorators.