TypeScript-Dekoratoren verstehen und implementieren für verbesserte Codemuster
Wenhao Wang
Dev Intern · Leapcell

Einleitung
In der sich ständig weiterentwickelnden Landschaft der modernen Webentwicklung ist die Erstellung von sauberem, wartbarem und skalierbarem Code von größter Bedeutung. Wenn Anwendungen komplexer werden, stoßen Entwickler häufig auf sich wiederholende Aufgaben wie das Protokollieren von Methodenaufrufen, das Validieren von Eingaben oder die Erzwingung von Zugriffskontrollen in verschiedenen Teilen ihrer Codebasis. Während die traditionelle objektorientierte Programmierung Mechanismen wie Vererbung und Komposition bietet, können diese manchmal zu Boilerplate-Code oder verknüpften Abhängigkeiten führen. Hier sind TypeScript-Dekoratoren eine leistungsstarke und elegante Lösung. Sie bieten eine deklarative Möglichkeit, Metadaten hinzuzufügen und das Verhalten von Klassen, Methoden, Eigenschaften oder Parametern zu ändern, ohne ihre ursprüngliche Implementierung zu verändern. Das Verständnis und die Nutzung von Dekoratoren können die Lesbarkeit des Codes erheblich verbessern, Redundanz reduzieren und robustere Architekturmuster fördern. Dieser Artikel untersucht die grundlegenden Konzepte hinter TypeScript-Dekoratoren, ihre zugrunde liegenden Mechanismen und demonstriert ihre praktische Anwendung anhand überzeugender Beispiele für Protokollierung und Berechtigungsprüfungen.
Kernkonzepte von TypeScript-Dekoratoren
Bevor wir uns mit den Details befassen, wollen wir ein klares Verständnis der Kernterminologie im Zusammenhang mit Dekoratoren schaffen.
- Dekorator: Eine spezielle Art von Deklaration, die an eine Klassendeklaration, Methode, einen Accessor, eine Eigenschaft oder einen Parameter angehängt werden kann. Dekoratoren werden mit einem
@
-Symbol gefolgt von einem Funktionsnamen gekennzeichnet. - Dekorator-Factory: Eine Funktion, die den Ausdruck zurückgibt, der zur Laufzeit vom Dekorator aufgerufen wird. Dies ermöglicht die Übergabe von Argumenten an den Dekorator.
- Target: Die Entität, auf die der Dekorator angewendet wird (z. B. der Klassenkonstruktor, der Methodendeskriptor, der Eigenschaftendeskriptor oder der Parameterindex).
- Eigenschaftendeskriptor: Ein Objekt, das die Attribute einer Eigenschaft beschreibt (z. B.
value
,writable
,enumerable
,configurable
). Dies ist für Methoden- und Accessordekoratoren relevant.
Dekoratoren sind im Wesentlichen Funktionen, die zur Deklarationszeit (wenn Ihr Code definiert wird, nicht wenn er ausgeführt wird) mit spezifischen Argumenten ausgeführt werden, abhängig davon, was sie dekorieren. Diese Ausführungsreihenfolge ist entscheidend für das Verständnis, wie sie Code ändern.
Die Funktionsweise von Dekoratoren
Auf fundamentaler Ebene, wenn TypeScript auf einen Dekorator stößt, transformiert es den dekorierten Code während der Kompilierung. Diese Transformation umschließt oder modifiziert im Wesentlichen das Ziel mithilfe der in der Dekoratorfunktion definierten Logik.
Betrachten wir die Ausführungsreihenfolge:
- Parameter-Dekoratoren werden zuerst für jeden Parameter angewendet.
- Methoden-, Accessor- oder Eigenschafts-Dekoratoren werden als Nächstes in der Reihenfolge ihrer Erscheinung angewendet.
- Klassen-Dekoratoren werden zuletzt angewendet.
Mehrere Dekoratoren für dasselbe Ziel werden von unten nach oben angewendet, d.h. der Dekorator, der der Deklaration am nächsten liegt, wird zuerst angewendet, und sein Ergebnis wird dann an den nächsten Dekorator darüber übergeben.
Implementierung von Dekoratoren: Eine Schritt-für-Schritt-Anleitung
Wir können einen Dekorator als Funktion definieren. Die Signatur dieser Funktion hängt davon ab, was sie dekoriert. Um die Dekoratorunterstützung zu aktivieren, stellen Sie sicher, dass Ihre tsconfig.json
Folgendes enthält:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true // Nützlich für Reflexion, aber für grundlegende Dekoratoren nicht unbedingt erforderlich } }
Klassen-Dekoratoren
Ein Klassen-Dekorator erhält als einziges Argument die Konstruktorfunktion der Klasse. Er kann eine Klassendefinition beobachten, ändern oder ersetzen.
function ClassLogger(constructor: Function) { console.log(`Class: ${constructor.name} was defined.`); // Sie können hier Eigenschaften, Methoden hinzufügen oder den Konstruktor ersetzen // Zur Demonstration protokollieren wir einfach. } @ClassLogger class UserService { constructor(public name: string) {} getUserName() { return this.name; } } const user = new UserService("Alice"); // Ausgabe: Class: UserService was defined. (zur Definitionszeit)
Methoden-Dekoratoren
Ein Methoden-Dekorator erhält drei Argumente:
target
: Der Prototyp der Klasse (für Instanzmethoden) oder die Konstruktorfunktion (für statische Methoden).propertyKey
: Der Name der Methode.descriptor
: Der Eigenschaftendeskriptor für die Methode.
Er kann die Methodendefinition inspizieren, ändern oder ersetzen.
function MethodLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; // Speichern Sie die ursprüngliche Methode console.log(`Decorating method: ${propertyKey} on class: ${target.constructor.name}`); descriptor.value = function(...args: any[]) { console.log(`Calling method: ${propertyKey} with arguments: ${JSON.stringify(args)}`); const result = originalMethod.apply(this, args); // Rufen Sie die ursprüngliche Methode auf console.log(`Method: ${propertyKey} returned: ${JSON.stringify(result)}`); return result; }; return descriptor; // Geben Sie den modifizierten Deskriptor zurück } class ProductService { constructor(private products: string[] = []) {} @MethodLogger getProduct(id: number): string | undefined { return this.products[id]; } @MethodLogger addProduct(name: string) { this.products.push(name); return `Added ${name}`; } } const productService = new ProductService(["Laptop", "Mouse"]); productService.getProduct(0); // Ausgabe: // Decorating method: getProduct on class: ProductService // Decorating method: addProduct on class: ProductService // Calling method: getProduct with arguments: [0] // Method: getProduct returned: "Laptop" productService.addProduct("Keyboard"); // Ausgabe: // Calling method: addProduct with arguments: ["Keyboard"] // Method: addProduct returned: "Added Keyboard"
Eigenschafts-Dekoratoren
Ein Eigenschafts-Dekorator erhält zwei Argumente:
target
: Der Prototyp der Klasse (für Instanzeigenschaften) oder die Konstruktorfunktion (für statische Eigenschaften).propertyKey
: Der Name der Eigenschaft.
Eigenschafts-Dekoratoren sind etwas eingeschränkt; sie können nur die deklarierte Eigenschaft beobachten, aber sie können den Eigenschaftendeskriptor nicht direkt ändern, da sie keinen erhalten. Sie können jedoch einen neuen Eigenschaftendeskriptor zurückgeben, wenn sie als Factory verwendet werden. Häufiger werden sie zum Registrieren von Metadaten oder zum Hinzufügen von Accessor-Funktionen verwendet.
function PropertyValidation(target: any, propertyKey: string) { let value: string; // Interner Speicher für die Eigenschaft const getter = function() { console.log(`Getting value for ${propertyKey}: ${value}`); return value; }; const setter = function(newVal: string) { if (newVal.length < 3) { console.warn(`Validation failed for ${propertyKey}: Value too short.`); } console.log(`Setting value for ${propertyKey}: ${newVal}`); value = newVal; }; // Ersetzen Sie die Eigenschaft durch einen Getter und Setter Object.defineProperty(target, propertyKey, { get: getter, set: setter, enumerable: true, configurable: true, }); } class User { @PropertyValidation username: string = ""; constructor(username: string) { this.username = username; } } const user2 = new User("Bob"); // Ausgabe: Setting value for username: Bob user2.username; // Ausgabe: Getting value for username: Bob user2.username = "Al"; // Ausgabe: Validation failed for username: Value too short. // Ausgabe: Setting value for username: Al user2.username = "Charlie"; // Ausgabe: Setting value for username: Charlie
Parameter-Dekoratoren
Ein Parameter-Dekorator erhält drei Argumente:
target
: Der Prototyp der Klasse (für Instanzmember) oder die Konstruktorfunktion (für statische Member).propertyKey
: Der Name der Methode.parameterIndex
: Der Index des Parameters in der Argumentliste der Methode.
Parameter-Dekoratoren werden typischerweise für Metadatenreflexion verwendet, z. B. zum Markieren von Parametern für Validierung oder Dependency Injection. Sie können den Parametertyp oder das Verhalten nicht direkt ändern.
function Required(target: Object, propertyKey: string | symbol, parameterIndex: number) { // Speichern Sie Metadaten über erforderliche Parameter const existingRequiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyKey) || []; existingRequiredParameters.push(parameterIndex); Reflect.defineMetadata("required", existingRequiredParameters, target, propertyKey); } // Benötigt die Installation von 'reflect-metadata': `npm install reflect-metadata --save` // und importieren Sie sie: `import "reflect-metadata";` class UserController { registerUser( @Required username: string, password: string, @Required email: string ) { console.log(`Registering user: ${username}, ${email}`); // ... Logik } } // Beispielverwendung (außerhalb des Dekorators, um Metadaten zu prüfen) function validate(instance: any, methodName: string, args: any[]) { const requiredParams: number[] = Reflect.getOwnMetadata("required", instance, methodName); if (requiredParams) { for (const index of requiredParams) { if (args[index] === undefined || args[index] === null || args[index] === "") { throw new Error(`Parameter at index ${index} is required for method ${methodName}.`); } } } } const userController = new UserController(); try { userController.registerUser("JohnDoe", "password123", "john.doe@example.com"); validate(userController, "registerUser", ["JohnDoe", "password123", "john.doe@example.com"]); userController.registerUser("", "pass", "email@test.com"); // Dies wird den Funktionsaufruf bestehen lassen, aber unsere benutzerdefinierte Validierungs-Hilfsfunktion wird fehlschlagen. validate(userController, "registerUser", ["", "pass", "email@test.com"]); } catch (error: any) { console.error(error.message); // Ausgabe: Parameter at index 0 is required for method registerUser. }
Praktische Anwendungen
Dekoratoren glänzen in Szenarien, in denen Sie wiederholt übergreifende Belange auf verschiedene Teile Ihrer Codebasis anwenden müssen.
Protokollierung von Methodenaufrufen
Wie oben mit MethodLogger
gezeigt, eignen sich Dekoratoren hervorragend zum automatischen Protokollieren der Ausführung von Methoden, einschließlich ihrer Argumente und Rückgabewerte. Dies ist für das Debugging, die Überwachung und die Auditierung von unschätzbarem Wert.
// Wiederverwendung des MethodLogger von oben für die Kürze // function MethodLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) { ... } class AuthService { private users: { [key: string]: string } = { admin: "securepass" }; @MethodLogger login(username: string, pass: string): boolean { if (this.users[username] === pass) { console.log(`User ${username} logged in successfully.`); return true; } console.warn(`Login failed for user ${username}.`); return false; } @MethodLogger changePassword(username: string, oldPass: string, newPass: string): boolean { if (this.users[username] === oldPass) { this.users[username] = newPass; console.log(`Password changed for user ${username}.`); return true; } console.error(`Failed to change password for user ${username}. Incorrect old password.`); return false; } } const authService = new AuthService(); authService.login("admin", "securepass"); authService.changePassword("admin", "securepass", "newSecurePass"); authService.login("admin", "wrongpass");
Dies protokolliert automatisch detaillierte Informationen zu login
- und changePassword
-Aufrufen, ohne deren Kernlogik zu ändern, wodurch die Geschäftslogik sauber bleibt.
Erzwingung der Zugriffskontrolle (Berechtigungen)
Methoden-Dekoratoren können verwendet werden, um rollenbasierte Zugriffskontrollen (RBAC) zu implementieren, indem geprüft wird, ob der aktuelle Benutzer über die erforderlichen Berechtigungen verfügt, bevor eine Methode ausgeführt wird. Dies erfordert oft eine Dekorator-Factory, um die erforderliche Rolle zu übergeben.
enum UserRole { Admin = "admin", Editor = "editor", Viewer = "viewer" } // Simulieren Sie die Rollen eines aktuellen Benutzers let currentUserRoles: UserRole[] = [UserRole.Editor]; function HasRole(requiredRole: UserRole) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { if (!currentUserRoles.includes(requiredRole)) { console.warn(`Access Denied: User does not have the '${requiredRole}' role to call ${propertyKey}.`); return; // Oder einen Fehler auslösen } return originalMethod.apply(this, args); }; return descriptor; }; } class AdminDashboard { @HasRole(UserRole.Admin) deleteUser(userId: string) { console.log(`Admin deleting user ${userId}...`); // ... tatsächliche Löschlogik } @HasRole(UserRole.Editor) editArticle(articleId: string, content: string) { console.log(`Editor editing article ${articleId}: ${content.substring(0, 20)}...`); // ... tatsächliche Bearbeitungslogik } @HasRole(UserRole.Viewer) viewReports() { console.log("Viewer accessing reports..."); // ... tatsächliche Berichtsaufruflogik } } const dashboard = new AdminDashboard(); console.log("Current User Roles:", currentUserRoles); dashboard.deleteUser("user123"); dashboard.editArticle("article456", "New article content..."); dashboard.viewReports(); console.log("\nChanging user roles to Admin..."); currentUserRoles = [UserRole.Admin]; dashboard.deleteUser("user123"); dashboard.editArticle("article456", "Updated content..."); // Admin hat auch Editor-Berechtigungen, wenn er über andere Mittel konfiguriert wird. dashboard.viewReports();
In diesem Beispiel prüft der HasRole
-Dekorator dynamisch die Benutzerberechtigungen, bevor er die Ausführung einer Methode zulässt. Dies zentralisiert die Berechtigungslogik und macht sie über viele Methoden hinweg wiederverwendbar und einfach anzuwenden.
Fazit
TypeScript-Dekoratoren bieten einen leistungsstarken und eleganten Mechanismus für die Metaprogrammierung, mit dem Entwickler Klassenmitglieder und Parameter deklarativ erweitern und ändern können. Durch die Trennung übergreifender Belange wie Protokollierung und Zugriffskontrolle von der Kernlogik fördern Dekoratoren saubereren, leichter wartbaren und hochgradig wiederverwendbaren Code. Die Einführung von Dekoratoren kann zu erheblichen Verbesserungen der Codearchitektur und der Effizienz von Entwicklern führen.