Klassen und Methoden mit @decorator transformieren
Nach jahrelangem Ringen im TC39-Komitee sind JavaScript Decorators in Stage 3 angekommen. Sie ermöglichen Metaprogrammierung auf Klassenebene ohne Babel-Hacks: Logging, Memoization, Validation und Dependency Injection als wiederverwendbare Annotationen.
Inhaltsverzeichnis
- 1. Was Decorators leisten und warum es so lange dauerte
- 2. Das Grundprinzip: Decorators als Funktionen
- 3. Class Decorators: Klassen transformieren
- 4. Method Decorators: Methoden wrappen
- 5. Field Decorators: Felder initialisieren
- 6. Accessor Decorators: getter und setter kontrollieren
- 7. Praxispatterns: Logging, Memoize, Validate
- 8. Decorators in TypeScript 5.x vs. Legacy Decorators
- 9. JavaScript Decorators im Ökosystem-Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Decorators leisten und warum es so lange dauerte
JavaScript Decorators sind eine Form von Metaprogrammierung: Sie erlauben es, Klassen, Methoden, Felder und Accessors zur Definitionszeit zu annotieren und zu transformieren, ohne den Klassenrumpf zu verändern. Das Konzept ist aus anderen Sprachen bekannt – Java-Annotations, Python-Decorators, C#-Attributes – und wurde in JavaScript-Frameworks wie Angular und NestJS über Jahre durch TypeScript-Decorators und Babel-Plugins simuliert. Die Standardisierung durch TC39 gestaltete sich schwierig, weil die ursprüngliche Stage-2-Spezifikation (2016) fundamentale Designprobleme hatte und vollständig überarbeitet werden musste.
Die neue Stage-3-Spezifikation (seit 2022, native Unterstützung ab Chrome 130, Safari 18, Firefox 131) ist inkompatibel mit dem alten TypeScript-experimentalDecorators-Modus. Das sorgt für Verwirrung: JavaScript Decorators im Sinne der TC39-Spezifikation und TypeScript-Decorators mit experimentalDecorators: true sind verschiedene Dinge. Wer heute mit TypeScript 5.0+ arbeitet und experimentalDecorators nicht explizit setzt, verwendet die neue TC39-Semantik – und muss die Unterschiede kennen, um Migration-Fallen zu vermeiden.
2. Das Grundprinzip: Decorators als Funktionen
Ein JavaScript Decorator ist eine Funktion, die beim Definieren einer Klasse oder eines Klassenelements aufgerufen wird. Sie erhält als Parameter die zu dekorierende Entität und ein Kontext-Objekt, das Metadaten über die Entität enthält. Der Rückgabewert ersetzt die ursprüngliche Entität – oder gibt nichts zurück, wenn keine Transformation stattfindet. Das Kontext-Objekt (context) enthält kind (der Typ: 'class', 'method', 'field', 'accessor', 'getter', 'setter'), name (der Bezeichner), static und private (booleans) sowie addInitializer(fn) – eine Funktion, mit der Code zur Instanz-Initialisierungszeit registriert wird.
Der entscheidende Unterschied zur alten Stage-2-Spezifikation: JavaScript Decorators in der neuen Spezifikation haben keinen Zugriff auf Prototype-Descriptor-Objekte und können keine neuen Properties zur Klasse hinzufügen. Sie können nur das ersetzen, was sie dekorieren, oder über addInitializer Code zur Instanziierungszeit ausführen. Das ist bewusst restriktiv – es macht Decorators sicherer und vorhersagbarer. Wer zur Kompilierzeit Properties hinzufügen will, nutzt Class Fields oder Static Blocks stattdessen.
// The anatomy of a JavaScript Decorator (TC39 Stage 3 spec)
// A method decorator receives: (value, context)
// value = the original method function
// context = { kind, name, static, private, addInitializer, access }
function readonly(value, context) {
if (context.kind === 'method') {
// Return a replacement function — or return nothing for annotation-only
return function (...args) {
return value.apply(this, args);
};
}
}
// A class decorator receives: (value, context)
// value = the class constructor
// Return a new class or undefined
function sealed(value, context) {
if (context.kind === 'class') {
Object.seal(value.prototype);
return value; // Return transformed class
}
}
// Usage — decorator applied at class definition time
@sealed
class ApiClient {
baseUrl = 'https://api.mironsoft.de';
@readonly
fetch(endpoint) {
return globalThis.fetch(`${this.baseUrl}${endpoint}`);
}
}
3. Class Decorators: Klassen transformieren
Class Decorators werden auf die Klasse als Ganzes angewandt und können die Klasse ersetzen, erweitern oder mit Metadaten anreichern. Sie erhalten den Klassen-Konstruktor als ersten Parameter und können eine neue Klasse (oder einen neuen Konstruktor) zurückgeben. Das ermöglicht Patterns wie automatisches Registrieren in einem globalen Registry, das Einfrieren des Prototyps, das Hinzufügen von Mixin-Verhalten oder das Wrappen des Konstruktors für Singleton-Patterns.
Ein wichtiges Konzept bei Class Decorators: Sie werden in umgekehrter Reihenfolge angewandt, wenn mehrere Decorators gestapelt sind. Der unterste Decorator (am nächsten zur Klasse) wird zuerst ausgeführt, der oberste zuletzt. Das entspricht dem mathematischen Kompositionsprinzip f(g(x)), wobei g zuerst angewandt wird. Für stateful Decorators, die Initialisierungslogik benötigen, ist context.addInitializer(fn) die richtige Ergänzung: Die registrierte Funktion wird nach dem Konstruktor-Aufruf für jede neue Instanz ausgeführt.
// Class decorator: register class in a global service registry
const registry = new Map();
function injectable(value, context) {
if (context.kind !== 'class') return;
// Register class by its name in the global registry
registry.set(context.name, value);
// Wrap constructor to track instantiation
return class extends value {
constructor(...args) {
super(...args);
console.log(`[DI] Instantiating ${context.name}`);
}
};
}
// Class decorator: singleton pattern
function singleton(value, context) {
let instance = null;
return class extends value {
constructor(...args) {
if (instance) return instance;
super(...args);
instance = this;
}
};
}
@injectable
@singleton
class DatabaseConnection {
constructor(url) {
this.url = url;
this.connected = false;
}
connect() {
this.connected = true;
console.log(`Connected to ${this.url}`);
}
}
// Both references point to the same instance (singleton)
const db1 = new DatabaseConnection('postgres://localhost/app');
const db2 = new DatabaseConnection('ignored');
console.log(db1 === db2); // true
4. Method Decorators: Methoden wrappen
Method Decorators sind die am häufigsten verwendete Decorator-Kategorie. Sie erhalten die originale Methoden-Funktion und können eine Ersatzfunktion zurückgeben, die die originale umhüllt. Das ermöglicht Cross-Cutting-Concerns wie Logging, Performance-Messung, Retry-Logik, Rate-Limiting, Error-Handling und Caching, ohne diese Logik in jede Methode einzubauen. Ein Method Decorator für Logging misst zum Beispiel die Ausführungszeit und loggt Argumente und Rückgabewert, ohne dass die Methode selbst davon weiß.
Das context.addInitializer-Muster ist bei Method Decorators besonders nützlich für Binding: Ein Decorator kann mit addInitializer sicherstellen, dass die Methode automatisch an die Instanz gebunden wird, wenn sie als Callback übergeben wird. Das löst das klassische this-Binding-Problem in Event-Listenern ohne manuelles .bind(this) oder Arrow-Function-Felder. Dieser Ansatz ist sauberer als die bisher verbreiteten Lösungen, weil er deklarativ ist und kein Boilerplate in jeder Klasse erfordert.
// Method decorator: performance logging
function measure(value, context) {
if (context.kind !== 'method') return;
const methodName = context.name;
return function (...args) {
const start = performance.now();
const result = value.apply(this, args);
if (result instanceof Promise) {
return result.finally(() => {
const duration = (performance.now() - start).toFixed(2);
console.log(`[Perf] ${String(methodName)}: ${duration}ms (async)`);
});
}
const duration = (performance.now() - start).toFixed(2);
console.log(`[Perf] ${String(methodName)}: ${duration}ms`);
return result;
};
}
// Method decorator: auto-bind to instance
function bound(value, context) {
if (context.kind !== 'method') return;
context.addInitializer(function () {
// 'this' refers to the instance at initialization time
this[context.name] = value.bind(this);
});
}
class DataService {
#baseUrl = 'https://api.mironsoft.de';
@measure
async fetchProducts(category) {
const res = await fetch(`${this.#baseUrl}/products?category=${category}`);
return res.json();
}
@bound
handleClick(event) {
// 'this' is always the DataService instance, even as a callback
console.log(this.#baseUrl, event.target);
}
}
const svc = new DataService();
// Safe to pass as callback without .bind(this)
document.addEventListener('click', svc.handleClick);
5. Field Decorators: Felder initialisieren
Field Decorators unterscheiden sich von Method Decorators grundlegend: Sie erhalten keinen Feldwert als ersten Parameter (da Felder zur Definitionszeit noch keinen Wert haben) und müssen stattdessen eine Initialisierungsfunktion zurückgeben, die für jede Instanz aufgerufen wird. Die Initialisierungsfunktion erhält den initialen Feldwert und gibt den neuen Wert zurück. Das ermöglicht Patterns wie Validierung, Transformation und Konvertierung von Feldwerten bei der Instantiierung.
Ein konkretes Beispiel für Field Decorators: Ein @clamp-Decorator, der sicherstellt, dass ein numerisches Feld innerhalb definierter Grenzen bleibt. Oder ein @serialize-Decorator, der eine Liste von zu serialisierenden Feldern in einem Class-Level-Metadaten-Store registriert. Letzteres erfordert das Zusammenspiel von Field Decorator und Class Decorator: Der Field Decorator registriert Metadaten, der Class Decorator liest sie aus und fügt der Klasse eine toJSON()-Methode hinzu. Dieses Muster ermöglicht deklaratives Serialisierungsverhalten ohne externe Schema-Definitionen.
6. Accessor Decorators: getter und setter kontrollieren
Das accessor-Schlüsselwort ist eine neue Ergänzung der Klassen-Syntax, die gemeinsam mit JavaScript Decorators eingeführt wurde. accessor name = value definiert ein Klassenfeld mit automatisch generierten getter und setter, die auf einer privaten internen Variablen operieren. Accessor Decorators können diese getter und setter wrappen – zum Beispiel um Wertänderungen zu beobachten (Observable-Pattern), Validierung beim Setzen durchzuführen oder den Zugriff zu loggen.
Der Accessor Decorator erhält ein Objekt mit get- und set-Methoden (die originalen getter und setter) und kann ein neues Objekt mit ersetzten getter und setter zurückgeben. Das ist das sauberste Muster für reaktive Properties in Vanilla-JavaScript ohne Framework: Ein @observable-Decorator, der bei jeder Wertänderung ein Custom Event dispatcht, gibt Klassen reaktives Verhalten mit einer einzigen Annotation – vergleichbar mit @property in Lit oder @observable in MobX, aber ohne externe Abhängigkeiten.
// Accessor decorator: observable property with CustomEvent
function observable(value, context) {
if (context.kind !== 'accessor') return;
const { get, set } = value;
const eventName = `change:${String(context.name)}`;
return {
get() {
return get.call(this);
},
set(newValue) {
const oldValue = get.call(this);
if (newValue === oldValue) return;
set.call(this, newValue);
// Dispatch a custom event on the element if it's a DOM node
if (this instanceof EventTarget) {
this.dispatchEvent(
new CustomEvent(eventName, {
bubbles: true,
detail: { oldValue, newValue, property: context.name },
})
);
}
},
init(value) {
return value; // Initial value unchanged
},
};
}
// Accessor decorator: type validation
function typed(expectedType) {
return function (value, context) {
if (context.kind !== 'accessor') return;
const { get, set } = value;
return {
get() { return get.call(this); },
set(newValue) {
if (typeof newValue !== expectedType) {
throw new TypeError(`${String(context.name)} must be ${expectedType}, got ${typeof newValue}`);
}
set.call(this, newValue);
},
init(v) { return v; },
};
};
}
class ProductModel {
@observable
accessor name = '';
@observable
@typed('number')
accessor price = 0;
}
const product = new ProductModel();
product.addEventListener('change:price', (e) => {
console.log(`Price changed: ${e.detail.oldValue} → ${e.detail.newValue}`);
});
product.price = 29.99; // Fires change:price event
product.price = 'invalid'; // TypeError: price must be number
7. Praxispatterns: Logging, Memoize, Validate
Die wertvollsten JavaScript Decorator-Patterns sind jene, die Cross-Cutting-Concerns aus der Geschäftslogik heraushalten. Logging ist das klassische Beispiel: Statt in jeder Methode manuell console.log aufzurufen, annotiert man kritische Methoden mit @log. Memoization löst das Performance-Problem bei reinen Funktionen, die mit denselben Argumenten immer dasselbe Ergebnis liefern: Ein @memoize-Decorator cacht Ergebnisse in einer WeakMap pro Instanz. Eingabevalidierung als @validate-Decorator prüft Argumente gegen ein Schema, bevor die eigentliche Methode aufgerufen wird.
Ein besonders elegantes Pattern für JavaScript Decorators in Webkomponenten: @eventHandler, der eine Methode als Event-Listener registriert und automatisch per addInitializer mit this bindet. In Verbindung mit dem Lifecycle einer Custom Element-Klasse können Decorators den gesamten Event-Listener-Lebenszyklus verwalten – Registrierung beim Element-Connect, Entfernung beim Disconnect – ohne dass diese Logik im Klassenrumpf sichtbar ist. Das macht Web-Component-Klassen deutlich lesbarer.
8. Decorators in TypeScript 5.x vs. Legacy Decorators
TypeScript 5.0 hat die TC39 Stage-3-Semantik für JavaScript Decorators implementiert. Wer bisher experimentalDecorators: true in der tsconfig.json gesetzt hat und Decorators aus NestJS, TypeORM oder anderen Frameworks nutzt, verwendet das alte, inkompatible Decorator-System. Die Migration ist nicht trivial: Alter und neuer Decorator-Code können nicht gemischt werden, und viele Frameworks hatten zum Zeitpunkt des TypeScript-5.0-Releases noch keine Unterstützung für das neue System.
Die wichtigsten Unterschiede: Alte TypeScript-Decorators erhalten target, propertyKey und descriptor – die neue Spezifikation verwendet (value, context). Alte Decorators können Properties hinzufügen; neue nicht. Alte Decorators sind mit reflect-metadata verknüpft; neue haben eigene Metadaten-Mechanismen über Symbol.metadata. Die Empfehlung für neue Projekte: experimentalDecorators nicht setzen und direkt mit der TC39-Semantik arbeiten. Für bestehende Projekte mit Framework-Abhängigkeiten den Framework-Migrationsguide abwarten.
| Merkmal | TC39 Stage 3 (neu) | TypeScript experimentalDecorators (alt) |
|---|---|---|
| Signatur | (value, context) | (target, key, descriptor) |
| Properties hinzufügen | Nicht möglich | Möglich |
| Metadaten | Symbol.metadata (nativ) | reflect-metadata (Polyfill) |
| addInitializer | Ja (im context) | Nein |
| accessor-Keyword | Ja | Nein |
| Native Browser-Unterstützung | Chrome 130+, Safari 18+ | Nie (Transpile-only) |
9. JavaScript Decorators im Ökosystem-Vergleich
Die Einführung von standardisierten JavaScript Decorators hat Auswirkungen auf das gesamte Framework-Ökosystem. Frameworks wie NestJS, TypeORM und Angular, die stark auf Decorators angewiesen sind, müssen auf die neue Semantik migrieren. Das ist aufwändig, weil die alten target/key/descriptor-Signaturen durch value/context ersetzt werden müssen und reflect-metadata durch Symbol.metadata abgelöst wird. Angular ab Version 19 und TypeScript 5.x sind der neuen Semantik bereits näher, aber vollständige Migrationen brauchen Zeit.
Für Projekte ohne Framework-Abhängigkeiten sind JavaScript Decorators heute produktionsreif, wenn Babel mit dem entsprechenden Plugin oder ein moderner Browser (Chrome 130+, Safari 18+) verwendet wird. Für Bibliotheksautoren bieten Decorators eine elegante API-Oberfläche: Konsumenten annotieren ihre Klassen deklarativ, die Bibliothek implementiert das Verhalten. Das ist das Prinzip hinter ORMs, Validierungs-Frameworks und DI-Containern – und mit nativen JavaScript Decorators sind diese Muster endlich ohne Transpiler-Abhängigkeiten möglich.
Mironsoft
Modernes JavaScript, TypeScript-Architektur und Framework-Migration
JavaScript Decorators in Ihr Projekt integrieren?
Wir implementieren TC39-konforme Decorator-Bibliotheken für Cross-Cutting-Concerns, migrieren bestehende experimentalDecorators-Codebasen und entwerfen Decorator-APIs für wiederverwendbare Klassenannotationen.
Decorator-Design
Logging, Memoization, Validation und DI als wiederverwendbare Annotationen
Migration
experimentalDecorators zu TC39-Stage-3 migrieren – sicher und schrittweise
Framework-Upgrade
Angular, NestJS und TypeORM auf neues Decorator-System updaten
10. Zusammenfassung
JavaScript Decorators in der TC39-Stage-3-Spezifikation sind die native Antwort auf das jahrelange Boilerplate-Problem bei Metaprogrammierung in JavaScript. Class Decorators transformieren Klassen, Method Decorators wrappen Methoden für Cross-Cutting-Concerns, Field Decorators kontrollieren die Initialisierung von Feldern, und Accessor Decorators geben getter und setter vollständige Observability. Das neue (value, context)-Signatursystem mit context.addInitializer und Symbol.metadata ist sauberer und sicherer als das alte TypeScript-experimentalDecorators-System, das auf reflect-metadata angewiesen war.
Der praktische Einsatz von JavaScript Decorators lohnt sich heute für neue Projekte mit Babel-Plugin oder modernen Browsern (Chrome 130+, Safari 18+). Für bestehende Codebasen mit Framework-Abhängigkeiten ist Abwarten auf Framework-seitige Migrationsguides ratsam. Die drei mächtigsten Patterns – @measure für Performance-Logging, @observable für reaktive Felder und @bound für automatisches this-Binding – lösen Alltagsprobleme in Klassenarchitekturen, ohne die Geschäftslogik mit Boilerplate zu verunreinigen.
JavaScript Decorators — Das Wichtigste auf einen Blick
Signatur
TC39-Decorator: (value, context). context.kind: 'class', 'method', 'field', 'accessor'. context.addInitializer(fn) für Instanz-Init-Code.
Reihenfolge
Mehrere Decorators werden von unten nach oben angewandt. @b @a class X → erst a, dann b. Mathematisches Kompositionsprinzip.
vs. Legacy
TC39: kein reflect-metadata, kein Hinzufügen von Properties, accessor-Keyword. experimentalDecorators: target/key/descriptor, Properties möglich, reflect-metadata.
Verfügbarkeit
Native: Chrome 130+, Safari 18+, Firefox 131+. Transpiliert: Babel-Plugin, TypeScript 5.0+ ohne experimentalDecorators.
11. FAQ: JavaScript Decorators
1Was sind JavaScript Decorators?
2TC39 vs. experimentalDecorators?
value/context. Legacy: target/key/descriptor.3Reihenfolge bei gestapelten Decorators?
@b @a class X — erst a wird angewandt, dann b.4Was ist context.addInitializer()?
5Was ist das accessor-Schlüsselwort?
accessor name = value – Auto-Property mit getter und setter über private Variable. Accessor Decorators können diese wrappen.