Udforsk kraften i TypeScript Decorators til metadata-programmering, aspektorienteret programmering og forbedring af kode med deklarative mønstre. En komplet guide.
TypeScript Decorators: Mestring af Metadata-programmeringsmønstre for Robuste Applikationer
I det store landskab af moderne softwareudvikling er det afgørende at vedligeholde rene, skalerbare og håndterbare kodebaser. TypeScript, med sit kraftfulde typesystem og avancerede funktioner, giver udviklere værktøjerne til at opnå dette. Blandt dets mest spændende og transformative funktioner er Decorators. Selvom det stadig er en eksperimentel funktion på tidspunktet for denne skrivelse (Stage 3-forslag til ECMAScript), anvendes decorators i vid udstrækning i frameworks som Angular og TypeORM, hvilket fundamentalt ændrer, hvordan vi tilgår designmønstre, metadata-programmering og aspektorienteret programmering (AOP).
Denne omfattende guide vil dykke dybt ned i TypeScript decorators, udforske deres mekanik, forskellige typer, praktiske anvendelser og bedste praksis. Uanset om du bygger store enterprise-applikationer, microservices eller web-grænseflader på klientsiden, vil forståelse af decorators give dig mulighed for at skrive mere deklarativ, vedligeholdelsesvenlig og kraftfuld TypeScript-kode.
Forståelse af Kernen: Hvad er en Decorator?
I sin kerne er en decorator en særlig type erklæring, der kan knyttes til en klassedeklaration, metode, accessor, property eller parameter. Decorators er funktioner, der returnerer en ny værdi (eller ændrer en eksisterende) for det mål, de dekorerer. Deres primære formål er at tilføje metadata eller ændre adfærden af den erklæring, de er knyttet til, uden direkte at ændre den underliggende kodestruktur. Denne eksterne, deklarative måde at udvide kode på er utroligt kraftfuld.
Tænk på decorators som annoteringer eller etiketter, du anvender på dele af din kode. Disse etiketter kan derefter læses eller handles på af andre dele af din applikation eller af frameworks, ofte ved kørselstid, for at levere yderligere funktionalitet eller konfiguration.
Syntaksen for en Decorator
Decorators har et @
-symbol som præfiks, efterfulgt af decorator-funktionens navn. De placeres umiddelbart før den erklæring, de dekorerer.
@MyDecorator\nclass MyClass {\n @AnotherDecorator\n myMethod() {\n // ...\n }\n}
Aktivering af Decorators i TypeScript
Før du kan bruge decorators, skal du aktivere compiler-indstillingen experimentalDecorators
i din tsconfig.json
-fil. Derudover skal du for avancerede metadata-refleksionsegenskaber (ofte brugt af frameworks) også bruge emitDecoratorMetadata
og reflect-metadata
-polyfill.
// tsconfig.json\n{\n "compilerOptions": {\n "target": "ES2017",\n "module": "commonjs",\n "experimentalDecorators": true,\n "emitDecoratorMetadata": true,\n "outDir": "./dist",\n "strict": true,\n "esModuleInterop": true,\n "skipLibCheck": true,\n "forceConsistentCasingInFileNames": true\n }\n}
Du skal også installere reflect-metadata
:
npm install reflect-metadata --save\n# or\nyarn add reflect-metadata
Og importere det helt øverst i din applikations startpunkt (f.eks. main.ts
eller app.ts
):
import "reflect-metadata";\n// Din applikationskode følger
Decorator Factories: Tilpasning lige ved hånden
Selvom en grundlæggende decorator er en funktion, har du ofte brug for at sende argumenter til en decorator for at konfigurere dens adfærd. Dette opnås ved at bruge en decorator factory. En decorator factory er en funktion, der returnerer den faktiske decorator-funktion. Når du anvender en decorator factory, kalder du den med dens argumenter, og den returnerer derefter den decorator-funktion, som TypeScript anvender på din kode.
Eksempel på at oprette en simpel Decorator Factory
Lad os oprette en factory til en Logger
-decorator, der kan logge meddelelser med forskellige præfikser.
function Logger(prefix: string) {\n return function (target: Function) {\n console.log(`[${prefix}] Klassen ${target.name} er blevet defineret.`);\n };\n}\n\n@Logger("APP_INIT")\nclass ApplicationBootstrap {\n constructor() {\n console.log("Applikationen starter...");\n }\n}\n\nconst app = new ApplicationBootstrap();\n// Output:\n// [APP_INIT] Klassen ApplicationBootstrap er blevet defineret.\n// Applikationen starter...
I dette eksempel er Logger("APP_INIT")
kaldet til decorator-factory'en. Den returnerer den faktiske decorator-funktion, som tager target: Function
(klassekonstruktøren) som sit argument. Dette giver mulighed for dynamisk konfiguration af decoratorens adfærd.
Typer af Decorators i TypeScript
TypeScript understøtter fem forskellige typer decorators, hver anvendelig på en bestemt type erklæring. Signaturen for decorator-funktionen varierer afhængigt af den kontekst, den anvendes i.
1. Klasse-decorators
Klasse-decorators anvendes på klassedeklarationer. Decorator-funktionen modtager klassens constructor som sit eneste argument. En klasse-decorator kan observere, modificere eller endda erstatte en klassedefinition.
Signatur:
function ClassDecorator(target: Function) { ... }
Returværdi:
Hvis klasse-decoratoren returnerer en værdi, vil den erstatte klassedeklarationen med den angivne constructor-funktion. Dette er en kraftfuld funktion, der ofte bruges til mixins eller klasse-udvidelse. Hvis der ikke returneres nogen værdi, bruges den oprindelige klasse.
Anvendelsesområder:
- Registrering af klasser i en dependency injection-container.
- Anvendelse af mixins eller yderligere funktionaliteter til en klasse.
- Framework-specifikke konfigurationer (f.eks. routing i et web-framework).
- Tilføjelse af livscyklus-hooks til klasser.
Eksempel på Klasse-decorator: Injektion af en Service
Forestil dig et simpelt dependency injection-scenarie, hvor du vil markere en klasse som "injectable" og valgfrit give den et navn i en container.
const InjectableServiceRegistry = new Map<string, Function>();\n\nfunction Injectable(name?: string) {\n return function<T extends { new(...args: any[]): {} }>(constructor: T) {\n const serviceName = name || constructor.name;\n InjectableServiceRegistry.set(serviceName, constructor);\n console.log(`Registreret service: ${serviceName}`);\n\n // Valgfrit kan du returnere en ny klasse her for at udvide funktionaliteten\n return class extends constructor {\n createdAt = new Date();\n // Yderligere properties eller metoder for alle injicerede services\n };\n };\n}\n\n@Injectable("UserService")\nclass UserDataService {\n getUsers() {\n return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];\n }\n}\n\n@Injectable()\nclass ProductDataService {\n getProducts() {\n return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];\n }\n}\n\nconsole.log("--- Services Registreret ---");\nconsole.log(Array.from(InjectableServiceRegistry.keys()));\n\nconst userServiceConstructor = InjectableServiceRegistry.get("UserService");\nif (userServiceConstructor) {\n const userServiceInstance = new userServiceConstructor();\n console.log("Brugere:", userServiceInstance.getUsers());\n // console.log("User Service oprettet den:", userServiceInstance.createdAt); // Hvis den returnerede klasse bruges\n}
Dette eksempel demonstrerer, hvordan en klasse-decorator kan registrere en klasse og endda modificere dens constructor. Injectable
-decoratoren gør klassen synlig for et teoretisk dependency injection-system.
2. Metode-decorators
Metode-decorators anvendes på metodedeklarationer. De modtager tre argumenter: målobjektet (for statiske medlemmer, constructor-funktionen; for instansmedlemmer, klassens prototype), navnet på metoden og metodens property descriptor.
Signatur:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Returværdi:
En metode-decorator kan returnere en ny PropertyDescriptor
. Hvis den gør det, vil denne descriptor blive brugt til at definere metoden. Dette giver dig mulighed for at modificere eller erstatte den oprindelige metodes implementering, hvilket gør den utroligt kraftfuld for AOP.
Anvendelsesområder:
- Logning af metodekald og deres argumenter/resultater.
- Caching af metoderesultater for at forbedre ydeevnen.
- Anvendelse af autorisationstjek før metodeudførelse.
- Måling af metodeudførelsestid.
- Debouncing eller throttling af metodekald.
Eksempel på Metode-decorator: Ydelsesmåling
Lad os oprette en MeasurePerformance
-decorator for at logge udførelsestiden for en metode.
function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n\n descriptor.value = function(...args: any[]) {\n const start = process.hrtime.bigint();\n const result = originalMethod.apply(this, args);\n const end = process.hrtime.bigint();\n const duration = Number(end - start) / 1_000_000;\n console.log(`Metoden \"${propertyKey}\" blev udført på ${duration.toFixed(2)} ms`);\n return result;\n };\n\n return descriptor;\n}\n\nclass DataProcessor {\n @MeasurePerformance\n processData(data: number[]): number[] {\n // Simulerer en kompleks, tidskrævende operation\n for (let i = 0; i < 1_000_000; i++) {\n Math.sin(i);\n }\n return data.map(n => n * 2);\n }\n\n @MeasurePerformance\n fetchRemoteData(id: string): Promise<string> {\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Data for ID: ${id}`);\n }, 500);\n });\n }\n}\n\nconst processor = new DataProcessor();\nprocessor.processData([1, 2, 3]);\nprocessor.fetchRemoteData("abc").then(result => console.log(result));
MeasurePerformance
-decoratoren ombryder den oprindelige metode med timing-logik og udskriver udførelsesvarigheden uden at rode forretningslogikken inde i selve metoden til. Dette er et klassisk eksempel på Aspektorienteret Programmering (AOP).
3. Accessor-decorators
Accessor-decorators anvendes på accessor- (get
og set
) deklarationer. Ligesom metode-decorators modtager de målobjektet, navnet på accessoren og dens property descriptor.
Signatur:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Returværdi:
En accessor-decorator kan returnere en ny PropertyDescriptor
, som vil blive brugt til at definere accessoren.
Anvendelsesområder:
- Validering ved indstilling af en property.
- Transformering af en værdi, før den indstilles, eller efter den er hentet.
- Kontrol af adgangstilladelser for properties.
Eksempel på Accessor-decorator: Caching af Getters
Lad os oprette en decorator, der cacher resultatet af en dyr getter-beregning.
function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalGetter = descriptor.get;\n const cacheKey = `_cached_${String(propertyKey)}`;\n\n if (originalGetter) {\n descriptor.get = function() {\n if (this[cacheKey] === undefined) {\n console.log(`[Cache Miss] Beregner værdi for ${String(propertyKey)}`);\n this[cacheKey] = originalGetter.apply(this);\n } else {\n console.log(`[Cache Hit] Bruger cachet værdi for ${String(propertyKey)}`);\n }\n return this[cacheKey];\n };\n }\n return descriptor;\n}\n\nclass ReportGenerator {\n private data: number[];\n\n constructor(data: number[]) {\n this.data = data;\n }\n\n // Simulerer en dyr beregning\n @CachedGetter\n get expensiveSummary(): number {\n console.log("Udfører dyr opsummeringsberegning...");\n return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;\n }\n}\n\nconst generator = new ReportGenerator([10, 20, 30, 40, 50]);\n\nconsole.log("Første adgang:", generator.expensiveSummary);\nconsole.log("Anden adgang:", generator.expensiveSummary);\nconsole.log("Tredje adgang:", generator.expensiveSummary);
Denne decorator sikrer, at beregningen for expensiveSummary
-getteren kun kører én gang; efterfølgende kald returnerer den cachede værdi. Dette mønster er meget nyttigt til at optimere ydeevnen, hvor adgang til en property involverer tunge beregninger eller eksterne kald.
4. Property-decorators
Property-decorators anvendes på property-deklarationer. De modtager to argumenter: målobjektet (for statiske medlemmer, constructor-funktionen; for instansmedlemmer, klassens prototype) og navnet på property'en.
Signatur:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
Returværdi:
Property-decorators kan ikke returnere nogen værdi. Deres primære anvendelse er at registrere metadata om property'en. De kan ikke direkte ændre property'ens værdi eller dens descriptor på dekorationstidspunktet, da descriptoren for en property endnu ikke er fuldt defineret, når property-decorators køres.
Anvendelsesområder:
- Registrering af properties til serialisering/deserialisering.
- Anvendelse af valideringsregler på properties.
- Indstilling af standardværdier eller konfigurationer for properties.
- ORM (Object-Relational Mapping) kolonne-mapping (f.eks.
@Column()
i TypeORM).
Eksempel på Property-decorator: Validering af påkrævede felter
Lad os oprette en decorator til at markere en property som "påkrævet" og derefter validere den ved kørselstid.
interface ValidationRule {\n property: string | symbol;\n validate: (value: any) => boolean;\n message: string;\n}\n\nconst validationRules: Map<Function, ValidationRule[]> = new Map();\n\nfunction Required(target: Object, propertyKey: string | symbol) {\n const rules = validationRules.get(target.constructor) || [];\n rules.push({\n property: propertyKey,\n validate: (value: any) => value !== null && value !== undefined && value !== "",\n message: `${String(propertyKey)} er påkrævet.`\n });\n validationRules.set(target.constructor, rules);\n}\n\nfunction validate(instance: any): string[] {\n const classRules = validationRules.get(instance.constructor) || [];\n const errors: string[] = [];\n\n for (const rule of classRules) {\n if (!rule.validate(instance[rule.property])) {\n errors.push(rule.message);\n }\n }\n return errors;\n}\n\nclass UserProfile {\n @Required\n firstName: string;\n\n @Required\n lastName: string;\n\n age?: number;\n\n constructor(firstName: string, lastName: string, age?: number) {\n this.firstName = firstName;\n this.lastName = lastName;\n this.age = age;\n }\n}\n\nconst user1 = new UserProfile("John", "Doe", 30);\nconsole.log("Valideringsfejl for Bruger 1:", validate(user1)); // []\n\nconst user2 = new UserProfile("", "Smith");\nconsole.log("Valideringsfejl for Bruger 2:", validate(user2)); // [\"firstName er påkrævet.\"]\n\nconst user3 = new UserProfile("Alice", "");\nconsole.log("Valideringsfejl for Bruger 3:", validate(user3)); // [\"lastName er påkrævet.\"]
Required
-decoratoren registrerer simpelthen valideringsreglen i et centralt validationRules
-map. En separat validate
-funktion bruger derefter disse metadata til at kontrollere instansen ved kørselstid. Dette mønster adskiller valideringslogik fra datadefinition, hvilket gør den genanvendelig og ren.
5. Parameter-decorators
Parameter-decorators anvendes på parametre i en klasses constructor eller en metode. De modtager tre argumenter: målobjektet (for statiske medlemmer, constructor-funktionen; for instansmedlemmer, klassens prototype), navnet på metoden (eller undefined
for constructor-parametre), og parameterens ordensindeks i funktionens parameterliste.
Signatur:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Returværdi:
Parameter-decorators kan ikke returnere nogen værdi. Ligesom property-decorators er deres primære rolle at tilføje metadata om parameteren.
Anvendelsesområder:
- Registrering af parametertyper til dependency injection (f.eks.
@Inject()
i Angular). - Anvendelse af validering eller transformation på specifikke parametre.
- Udtrækning af metadata om API-anmodningsparametre i web-frameworks.
Eksempel på Parameter-decorator: Injektion af anmodningsdata
Lad os simulere, hvordan et web-framework kan bruge parameter-decorators til at injicere specifikke data i en metodeparameter, såsom et bruger-ID fra en anmodning.
interface ParameterMetadata {\n index: number;\n key: string | symbol;\n resolver: (request: any) => any;\n}\n\nconst parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();\n\nfunction RequestParam(paramName: string) {\n return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {\n const targetKey = propertyKey || "constructor";\n let methodResolvers = parameterResolvers.get(target.constructor);\n if (!methodResolvers) {\n methodResolvers = new Map();\n parameterResolvers.set(target.constructor, methodResolvers);\n }\n const paramMetadata = methodResolvers.get(targetKey) || [];\n paramMetadata.push({\n index: parameterIndex,\n key: targetKey,\n resolver: (request: any) => request[paramName]\n });\n methodResolvers.set(targetKey, paramMetadata);\n };\n}\n\n// En hypotetisk framework-funktion til at kalde en metode med resolvede parametre\nfunction executeWithParams(instance: any, methodName: string, request: any) {\n const classResolvers = parameterResolvers.get(instance.constructor);\n if (!classResolvers) {\n return (instance[methodName] as Function).apply(instance, []);\n }\n const methodParamMetadata = classResolvers.get(methodName);\n if (!methodParamMetadata) {\n return (instance[methodName] as Function).apply(instance, []);\n }\n\n const args: any[] = Array(methodParamMetadata.length);\n for (const meta of methodParamMetadata) {\n args[meta.index] = meta.resolver(request);\n }\n return (instance[methodName] as Function).apply(instance, args);\n}\n\nclass UserController {\n getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {\n console.log(`Henter bruger med ID: ${userId}, Token: ${authToken || \"N/A\"}`);\n return { id: userId, name: "Jane Doe" };\n }\n\n deleteUser(@RequestParam("id") userId: string) {\n console.log(`Sletter bruger med ID: ${userId}`);\n return { status: "slettet", id: userId };\n }\n}\n\nconst userController = new UserController();\n\n// Simulerer en indkommende anmodning\nconst mockRequest = {\n id: "user123",\n token: "abc-123",\n someOtherProp: "xyz"\n};\n\nconsole.log("\n--- Udfører getUser ---");\nexecuteWithParams(userController, "getUser", mockRequest);\n\nconsole.log("\n--- Udfører deleteUser ---");\nexecuteWithParams(userController, "deleteUser", { id: "user456" });
Dette eksempel viser, hvordan parameter-decorators kan indsamle information om påkrævede metodeparametre. Et framework kan derefter bruge disse indsamlede metadata til automatisk at resolvere og injicere passende værdier, når metoden kaldes, hvilket markant forenkler controller- eller service-logik.
Sammensætning og Udførelsesrækkefølge af Decorators
Decorators kan anvendes i forskellige kombinationer, og det er afgørende at forstå deres udførelsesrækkefølge for at kunne forudsige adfærd og undgå uventede problemer.
Flere Decorators på et Enkelt Mål
Når flere decorators anvendes på en enkelt deklaration (f.eks. en klasse, metode eller property), udføres de i en bestemt rækkefølge: fra bund til top, eller højre til venstre, for deres evaluering. Deres resultater anvendes dog i modsat rækkefølge.
@DecoratorA\n@DecoratorB\nclass MyClass {\n // ...\n}\n
Her vil DecoratorB
blive evalueret først, derefter DecoratorA
. Hvis de ændrer klassen (f.eks. ved at returnere en ny constructor), vil ændringen fra DecoratorA
ombryde eller anvendes over ændringen fra DecoratorB
.
Eksempel: Kædning af Metode-decorators
Overvej to metode-decorators: LogCall
og Authorization
.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n console.log(`[LOG] Kalder ${String(propertyKey)} med args:`, args);\n const result = originalMethod.apply(this, args);\n console.log(`[LOG] Metode ${String(propertyKey)} returnerede:`, result);\n return result;\n };\n return descriptor;\n}\n\nfunction Authorization(roles: string[]) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n const currentUserRoles = ["admin"]; // Simulerer hentning af nuværende brugerroller\n const authorized = roles.some(role => currentUserRoles.includes(role));\n if (!authorized) {\n console.warn(`[AUTH] Adgang nægtet for ${String(propertyKey)}. Krævede roller: ${roles.join(", ")}`);\n throw new Error("Uautoriseret adgang");\n }\n console.log(`[AUTH] Adgang givet for ${String(propertyKey)}`);\n return originalMethod.apply(this, args);\n };\n return descriptor;\n };\n}\n\nclass SecureService {\n @LogCall\n @Authorization(["admin"])\n deleteSensitiveData(id: string) {\n console.log(`Sletter følsomme data for ID: ${id}`);\n return `Data ID ${id} slettet.`;\n }\n\n @Authorization(["user"])\n @LogCall // Rækkefølge ændret her\n fetchPublicData(query: string) {\n console.log(`Henter offentlige data med forespørgsel: ${query}`);\n return `Offentlige data for forespørgsel: ${query}`; \n }\n}\n\nconst service = new SecureService();\n\ntry {\n console.log("\n--- Kalder deleteSensitiveData (Admin Bruger) ---");\n service.deleteSensitiveData("record123");\n} catch (error: any) {\n console.error(error.message);\n}\n\ntry {\n console.log("\n--- Kalder fetchPublicData (Ikke-Admin Bruger) ---");\n // Simulerer en ikke-admin bruger, der forsøger at tilgå fetchPublicData, som kræver 'user'-rollen\n const mockUserRoles = ["guest"]; // Dette vil fejle auth\n // For at gøre dette dynamisk, ville du have brug for et DI-system eller statisk kontekst for nuværende brugerroller.\n // For enkelthedens skyld antager vi, at Authorization-decoratoren har adgang til nuværende brugerkontekst.\n // Lad os justere Authorization-decoratoren til altid at antage 'admin' for demoformål, \n // så det første kald lykkes, og det andet fejler for at vise forskellige stier.\n \n // Kør igen med brugerrolle, for at fetchPublicData lykkes.\n // Forestil dig, at currentUserRoles i Authorization bliver: ['user']\n // For dette eksempel, lad os holde det simpelt og vise rækkefølgeeffekten.\n service.fetchPublicData("søgeterm"); // Dette vil udføre Auth -> Log\n} catch (error: any) {\n console.error(error.message);\n}\n\n/* Forventet output for deleteSensitiveData:\n[AUTH] Adgang givet for deleteSensitiveData\n[LOG] Kalder deleteSensitiveData med args: [ 'record123' ]\nSletter følsomme data for ID: record123\n[LOG] Metode deleteSensitiveData returnerede: Data ID record123 slettet.\n*/\n\n/* Forventet output for fetchPublicData (hvis bruger har 'user'-rollen):\n[LOG] Kalder fetchPublicData med args: [ 'søgeterm' ]\n[AUTH] Adgang givet for fetchPublicData\nHenter offentlige data med forespørgsel: søgeterm\n[LOG] Metode fetchPublicData returnerede: Offentlige data for forespørgsel: søgeterm\n*/
Bemærk rækkefølgen: for deleteSensitiveData
kører Authorization
(nederst) først, derefter ombryder LogCall
(øverst) den. Den indre logik i Authorization
udføres først. For fetchPublicData
kører LogCall
(nederst) først, derefter ombryder Authorization
(øverst) den. Dette betyder, at LogCall
-aspektet vil være uden for Authorization
-aspektet. Denne forskel er kritisk for tværgående bekymringer som logning eller fejlhåndtering, hvor udførelsesrækkefølgen kan have en betydelig indvirkning på adfærden.
Udførelsesrækkefølge for Forskellige Mål
Når en klasse, dens medlemmer og parametre alle har decorators, er udførelsesrækkefølgen veldefineret:
- Parameter-decorators anvendes først, for hver parameter, startende fra den sidste parameter til den første.
- Derefter anvendes Metode-, Accessor- eller Property-decorators for hvert medlem.
- Til sidst anvendes Klasse-decorators på selve klassen.
Inden for hver kategori anvendes flere decorators på det samme mål fra bund til top (eller højre til venstre).
Eksempel: Fuld Udførelsesrækkefølge
function log(message: string) {\n return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {\n if (typeof descriptorOrIndex === 'number') {\n console.log(`Param Decorator: ${message} på parameter #${descriptorOrIndex} af ${String(propertyKey || \"constructor\")}`);\n } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {\n if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {\n console.log(`Metode/Accessor Decorator: ${message} på ${String(propertyKey)}`);\n } else {\n console.log(`Property Decorator: ${message} på ${String(propertyKey)}`);\n }\n } else {\n console.log(`Klasse Decorator: ${message} på ${target.name}`);\n }\n return descriptorOrIndex; // Returner descriptor for metode/accessor, undefined for andre\n };\n}\n\n@log("Klasse Niveau D")\n@log("Klasse Niveau C")\nclass MyDecoratedClass {\n @log("Statisk Property A")\n static staticProp: string = "";\n\n @log("Instans Property B")\n instanceProp: number = 0;\n\n @log("Metode D")\n @log("Metode C")\n myMethod(\n @log("Parameter Z") paramZ: string,\n @log("Parameter Y") paramY: number\n ) {\n console.log("Metoden myMethod blev udført.");\n }\n\n @log("Getter/Setter F")\n get myAccessor() {\n return "";\n }\n\n set myAccessor(value: string) {\n //...\n }\n\n constructor() {\n console.log("Constructor blev udført.");\n }\n}\n\nnew MyDecoratedClass();\n// Kald metode for at udløse metode-decorator\nnew MyDecoratedClass().myMethod("hello", 123);\n\n/* Forudsagt Output-rækkefølge (omtrentlig, afhængig af specifik TypeScript-version og kompilering):\nParam Decorator: Parameter Y på parameter #1 af myMethod\nParam Decorator: Parameter Z på parameter #0 af myMethod\nProperty Decorator: Statisk Property A på staticProp\nProperty Decorator: Instans Property B på instanceProp\nMetode/Accessor Decorator: Getter/Setter F på myAccessor\nMetode/Accessor Decorator: Metode C på myMethod\nMetode/Accessor Decorator: Metode D på myMethod\nKlasse Decorator: Klasse Niveau C på MyDecoratedClass\nKlasse Decorator: Klasse Niveau D på MyDecoratedClass\nConstructor blev udført.\nMetoden myMethod blev udført.\n*/
Den præcise timing for konsol-logs kan variere lidt afhængigt af, hvornår en constructor eller metode kaldes, men den rækkefølge, som decorator-funktionerne selv udføres i (og dermed deres sideeffekter eller returnerede værdier anvendes), følger reglerne ovenfor.
Praktiske Anvendelser og Designmønstre med Decorators
Decorators, især i kombination med reflect-metadata
-polyfill, åbner op for en ny verden af metadata-drevet programmering. Dette giver mulighed for kraftfulde designmønstre, der abstraherer boilerplate og tværgående bekymringer væk.
1. Dependency Injection (DI)
En af de mest fremtrædende anvendelser af decorators er i Dependency Injection-frameworks (som Angulars @Injectable()
, @Component()
, etc., eller NestJS's omfattende brug af DI). Decorators giver dig mulighed for at erklære afhængigheder direkte på constructors eller properties, hvilket gør det muligt for frameworket automatisk at instantiere og levere de korrekte services.
Eksempel: Forenklet Service-injektion
import "reflect-metadata"; // Essentiel for emitDecoratorMetadata\n\nconst INJECTABLE_METADATA_KEY = Symbol("injectable");\nconst INJECT_METADATA_KEY = Symbol("inject");\n\nfunction Injectable() {\n return function (target: Function) {\n Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);\n };\n}\n\nfunction Inject(token: any) {\n return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\n const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];\n existingInjections[parameterIndex] = token;\n Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);\n };\n}\n\nclass Container {\n private static instances = new Map<any, any>();\n\n static resolve<T>(target: { new (...args: any[]): T }): T {\n if (Container.instances.has(target)) {\n return Container.instances.get(target);\n }\n\n const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);\n if (!isInjectable) {\n throw new Error(`Klassen ${target.name} er ikke markeret som @Injectable.`);\n }\n\n // Hent constructor-parametrers typer (kræver emitDecoratorMetadata)\n const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];\n const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];\n\n const dependencies = paramTypes.map((paramType, index) => {\n // Brug eksplicit @Inject-token hvis angivet, ellers udled typen\n const token = explicitInjections[index] || paramType;\n if (token === undefined) {\n throw new Error(`Kan ikke resolvere parameter ved indeks ${index} for ${target.name}. Det kan være en cirkulær afhængighed eller primitiv type uden eksplicit @Inject.`);\n }\n return Container.resolve(token);\n });\n\n const instance = new target(...dependencies);\n Container.instances.set(target, instance);\n return instance;\n }\n}\n\n// Definer services\n@Injectable()\nclass DatabaseService {\n connect() {\n console.log("Forbinder til database...");\n return "DB Forbindelse";\n }\n}\n\n@Injectable()\nclass AuthService {\n private db: DatabaseService;\n\n constructor(db: DatabaseService) {\n this.db = db;\n }\n\n login() {\n console.log(`AuthService: Godkender ved hjælp af ${this.db.connect()}`);\n return "Bruger logget ind";\n }\n}\n\n@Injectable()\nclass UserService {\n private authService: AuthService;\n private dbService: DatabaseService; // Eksempel på injektion via property ved hjælp af en custom decorator eller framework-funktion\n\n constructor(@Inject(AuthService) authService: AuthService,\n @Inject(DatabaseService) dbService: DatabaseService) {\n this.authService = authService;\n this.dbService = dbService;\n }\n\n getUserProfile() {\n this.authService.login();\n this.dbService.connect();\n console.log("UserService: Henter brugerprofil...");\n return { id: 1, name: "Global Bruger" };\n }\n}\n\n// Resolv hoved-servicen\nconsole.log("--- Resolver UserService ---");\nconst userService = Container.resolve(UserService);\nconsole.log(userService.getUserProfile());\n\nconsole.log("\n--- Resolver AuthService (skal være cachet) ---");\nconst authService = Container.resolve(AuthService);\nauthService.login();
Dette detaljerede eksempel demonstrerer, hvordan @Injectable
- og @Inject
-decorators, kombineret med reflect-metadata
, giver en brugerdefineret Container
mulighed for automatisk at resolvere og levere afhængigheder. De design:paramtypes
-metadata, der automatisk udsendes af TypeScript (når emitDecoratorMetadata
er sat til true), er afgørende her.
2. Aspektorienteret Programmering (AOP)
AOP fokuserer på at modularisere tværgående bekymringer (f.eks. logning, sikkerhed, transaktioner), der går på tværs af flere klasser og moduler. Decorators passer perfekt til at implementere AOP-koncepter i TypeScript.
Eksempel: Logning med Metode-decorator
Hvis vi vender tilbage til LogCall
-decoratoren, er det et perfekt eksempel på AOP. Den tilføjer logningsadfærd til enhver metode uden at ændre metodens oprindelige kode. Dette adskiller "hvad der skal gøres" (forretningslogik) fra "hvordan det skal gøres" (logning, ydelsesmåling osv.).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n console.log(`[LOG AOP] Går ind i metode: ${String(propertyKey)} med args:`, args);\n try {\n const result = originalMethod.apply(this, args);\n console.log(`[LOG AOP] Forlader metode: ${String(propertyKey)} med resultat:`, result);\n return result;\n } catch (error: any) {\n console.error(`[LOG AOP] Fejl i metode ${String(propertyKey)}:`, error.message);\n throw error;\n }\n };\n return descriptor;\n}\n\nclass PaymentProcessor {\n @LogMethod\n processPayment(amount: number, currency: string) {\n if (amount <= 0) {\n throw new Error("Betalingsbeløb skal være positivt.");\n }\n console.log(`Behandler betaling på ${amount} ${currency}...`);\n return `Betaling på ${amount} ${currency} behandlet succesfuldt.`;\n }\n\n @LogMethod\n refundPayment(transactionId: string) {\n console.log(`Refunderer betaling for transaktions-ID: ${transactionId}...`);\n return `Refundering igangsat for ${transactionId}.`;\n }\n}\n\nconst processor = new PaymentProcessor();\nprocessor.processPayment(100, "USD");\ntry {\n processor.processPayment(-50, "EUR");\n} catch (error: any) {\n console.error("Fangede fejl:", error.message);\n}
Denne tilgang holder PaymentProcessor
-klassen fokuseret udelukkende på betalingslogik, mens LogMethod
-decoratoren håndterer den tværgående bekymring om logning.
3. Validering og Transformation
Decorators er utroligt nyttige til at definere valideringsregler direkte på properties eller til at transformere data under serialisering/deserialisering.
Eksempel: Datavalidering med Property-decorators
@Required
-eksemplet tidligere demonstrerede allerede dette. Her er et andet eksempel med en numerisk intervalvalidering.
interface FieldValidationRule {\n property: string | symbol;\n validator: (value: any) => boolean;\n message: string;\n}\n\nconst fieldValidationRules = new Map<Function, FieldValidationRule[]>();\n\nfunction addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {\n const rules = fieldValidationRules.get(target.constructor) || [];\n rules.push({ property: propertyKey, validator, message });\n fieldValidationRules.set(target.constructor, rules);\n}\n\nfunction IsPositive(target: Object, propertyKey: string | symbol) {\n addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} skal være et positivt tal.`);\n}\n\nfunction MaxLength(maxLength: number) {\n return function (target: Object, propertyKey: string | symbol) {\n addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} må højst være ${maxLength} tegn langt.`);\n };\n}\n\nclass Product {\n @MaxLength(50)\n name: string;\n\n @IsPositive\n price: number;\n\n constructor(name: string, price: number) {\n this.name = name;\n this.price = price;\n }\n\n static validate(instance: any): string[] {\n const errors: string[] = [];\n const rules = fieldValidationRules.get(instance.constructor) || [];\n for (const rule of rules) {\n if (!rule.validator(instance[rule.property])) {\n errors.push(rule.message);\n }\n }\n return errors;\n }\n}\n\nconst product1 = new Product("Laptop", 1200);\nconsole.log("Produkt 1 fejl:", Product.validate(product1)); // []\n\nconst product2 = new Product("Meget langt produktnavn der overskrider halvtreds tegns grænsen for testformål", 50);\nconsole.log("Produkt 2 fejl:", Product.validate(product2)); // [\"name må højst være 50 tegn langt.\"]\n\nconst product3 = new Product("Bog", -10);\nconsole.log("Produkt 3 fejl:", Product.validate(product3)); // [\"price skal være et positivt tal.\"]
Denne opsætning giver dig mulighed for deklarativt at definere valideringsregler på dine model-properties, hvilket gør dine datamodeller selvbeskrivende med hensyn til deres begrænsninger.
Bedste Praksis og Overvejelser
Selvom decorators er kraftfulde, bør de bruges med omtanke. Misbrug af dem kan føre til kode, der er sværere at debugge eller forstå.
Hvornår man skal bruge Decorators (og hvornår man ikke skal)
- Brug dem til:
- Tværgående bekymringer: Logning, caching, autorisation, transaktionsstyring.
- Metadata-deklaration: Definition af skema for ORM'er, valideringsregler, DI-konfiguration.
- Framework-integration: Når du bygger eller bruger frameworks, der udnytter metadata.
- Reducering af boilerplate: Abstrahering af gentagne kodemønstre.
- Undgå dem til:
- Simple funktionskald: Hvis et almindeligt funktionskald kan opnå det samme resultat tydeligt, foretræk det.
- Forretningslogik: Decorators bør udvide, ikke definere, kerneforretningslogik.
- Overkomplicering: Hvis brugen af en decorator gør koden mindre læsbar eller sværere at teste, genovervej.
Ydelsesmæssige Konsekvenser
Decorators udføres ved kompileringstid (eller definitionstid i JavaScript-runtime, hvis transpileret). Transformationen eller metadataindsamlingen sker, når klassen/metoden defineres, ikke ved hvert kald. Derfor er runtime-ydelsespåvirkningen af at *anvende* decorators minimal. Dog kan *logikken inden i* dine decorators have en ydelsespåvirkning, især hvis de udfører dyre operationer ved hvert metodekald (f.eks. komplekse beregninger inden i en metode-decorator).
Vedligeholdelse og Læsbarhed
Decorators kan, når de bruges korrekt, forbedre læsbarheden markant ved at flytte boilerplate-kode ud af hovedlogikken. Men hvis de udfører komplekse, skjulte transformationer, kan debugging blive en udfordring. Sørg for, at dine decorators er veldokumenterede, og at deres adfærd er forudsigelig.
Eksperimentel Status og Fremtiden for Decorators
Det er vigtigt at gentage, at TypeScript-decorators er baseret på et Stage 3 TC39-forslag. Dette betyder, at specifikationen er stort set stabil, men stadig kan undergå mindre ændringer, før den bliver en del af den officielle ECMAScript-standard. Frameworks som Angular har omfavnet dem og satser på deres eventuelle standardisering. Dette indebærer en vis risiko, selvom betydelige brud på bagudkompatibilitet er usandsynlige, givet deres udbredte anvendelse.
TC39-forslaget har udviklet sig. TypeScript's nuværende implementering er baseret på en ældre version af forslaget. Der er en skelnen mellem "Legacy Decorators" og "Standard Decorators". Når den officielle standard lander, vil TypeScript sandsynligvis opdatere sin implementering. For de fleste udviklere, der bruger frameworks, vil denne overgang blive håndteret af selve frameworket. For biblioteksforfattere kan det blive nødvendigt at forstå de subtile forskelle mellem legacy og fremtidige standard-decorators.
Compiler-indstillingen emitDecoratorMetadata
Denne indstilling, når den er sat til true
i tsconfig.json
, instruerer TypeScript-compileren i at udsende visse design-time type-metadata i den kompilerede JavaScript. Disse metadata inkluderer typen af constructor-parametrene (design:paramtypes
), returtypen for metoder (design:returntype
) og typen af properties (design:type
).
Disse udsendte metadata er ikke en del af standard JavaScript-runtime. De forbruges typisk af reflect-metadata
-polyfill, som derefter gør dem tilgængelige via Reflect.getMetadata()
-funktionerne. Dette er absolut afgørende for avancerede mønstre som Dependency Injection, hvor en container skal kende typerne af afhængigheder, en klasse kræver, uden eksplicit konfiguration.
Avancerede Mønstre med Decorators
Decorators kan kombineres og udvides for at bygge endnu mere sofistikerede mønstre.
1. Dekorering af Decorators (Højere-ordens Decorators)
Du kan oprette decorators, der ændrer eller sammensætter andre decorators. Dette er mindre almindeligt, men demonstrerer den funktionelle natur af decorators.
// En decorator, der sikrer, at en metode logges og også kræver admin-roller\nfunction AdminAndLoggedMethod() {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n // Anvend Authorization først (indre)\n Authorization(["admin"])(target, propertyKey, descriptor);\n // Anvend derefter LogCall (ydre)\n LogCall(target, propertyKey, descriptor);\n\n return descriptor; // Returner den modificerede descriptor\n };\n}\n\nclass AdminPanel {\n @AdminAndLoggedMethod()\n deleteUserAccount(userId: string) {\n console.log(`Sletter brugerkonto: ${userId}`);\n return `Bruger ${userId} slettet.`;\n }\n}\n\nconst adminPanel = new AdminPanel();\nadminPanel.deleteUserAccount("user007");\n/* Forventet Output (antaget admin-rolle):\n[AUTH] Adgang givet for deleteUserAccount\n[LOG] Kalder deleteUserAccount med args: [ 'user007' ]\nSletter brugerkonto: user007\n[LOG] Metode deleteUserAccount returnerede: Bruger user007 slettet.\n*/
Her er AdminAndLoggedMethod
en factory, der returnerer en decorator, og inden i den decorator anvender den to andre decorators. Dette mønster kan indkapsle komplekse decorator-sammensætninger.
2. Brug af Decorators til Mixins
Selvom TypeScript tilbyder andre måder at implementere mixins på, kan decorators bruges til at injicere kapabiliteter i klasser på en deklarativ måde.
function ApplyMixins(constructors: Function[]) {\n return function (derivedConstructor: Function) {\n constructors.forEach(baseConstructor => {\n Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {\n Object.defineProperty(\n derivedConstructor.prototype,\n name,\n Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)\n );\n });\n });\n };\n}\n\nclass Disposable {\n isDisposed: boolean = false;\n dispose() {\n this.isDisposed = true;\n console.log("Objekt kasseret.");\n }\n}\n\nclass Loggable {\n log(message: string) {\n console.log(`[Loggable] ${message}`);\n }\n}\n\n@ApplyMixins([Disposable, Loggable])\nclass MyResource implements Disposable, Loggable {\n // Disse properties/metoder injiceres af decoratoren\n isDisposed!: boolean;\n dispose!: () => void;\n log!: (message: string) => void;\n\n constructor(public name: string) {\n this.log(`Ressource ${this.name} oprettet.`);\n }\n\n cleanUp() {\n this.dispose();\n this.log(`Ressource ${this.name} ryddet op.`);\n }\n}\n\nconst resource = new MyResource("NetworkConnection");\nconsole.log(`Er kasseret: ${resource.isDisposed}`);\nresource.cleanUp();\nconsole.log(`Er kasseret: ${resource.isDisposed}`);
Denne @ApplyMixins
-decorator kopierer dynamisk metoder og properties fra basis-constructors til den afledte klasses prototype og "mixer" effektivt funktionaliteter ind.
Konklusion: Styrkelse af Moderne TypeScript-udvikling
TypeScript decorators er en kraftfuld og udtryksfuld funktion, der muliggør et nyt paradigme inden for metadata-drevet og aspektorienteret programmering. De giver udviklere mulighed for at forbedre, modificere og tilføje deklarativ adfærd til klasser, metoder, properties, accessors og parametre uden at ændre deres kerne-logik. Denne adskillelse af bekymringer fører til renere, mere vedligeholdelsesvenlig og yderst genanvendelig kode.
Fra at forenkle dependency injection og implementere robuste valideringssystemer til at tilføje tværgående bekymringer som logning og ydelsesmåling, tilbyder decorators en elegant løsning på mange almindelige udviklingsudfordringer. Selvom deres eksperimentelle status kræver opmærksomhed, signalerer deres udbredte anvendelse i store frameworks deres praktiske værdi og fremtidige relevans.
Ved at mestre TypeScript decorators får du et betydeligt værktøj i dit arsenal, der gør dig i stand til at bygge mere robuste, skalerbare og intelligente applikationer. Omfavn dem ansvarligt, forstå deres mekanik, og lås op for et nyt niveau af deklarativ kraft i dine TypeScript-projekter.