Utforska TypeScript-dekoratorer för metaprogrammering, aspektorienterad programmering och att berika kod med deklarativa mönster. Omfattande guide för utvecklare.
TypeScript-dekoratorer: BemÀstra metaprogrammeringsmönster för robusta applikationer
I det stora landskapet av modern programvaruutveckling Àr det avgörande att upprÀtthÄlla rena, skalbara och hanterbara kodbaser. TypeScript, med sitt kraftfulla typsystem och avancerade funktioner, förser utvecklare med verktyg för att uppnÄ detta. Bland dess mest fascinerande och omvÀlvande funktioner finns dekoratorer. Trots att de vid skrivande stund fortfarande Àr en experimentell funktion (Stage 3-förslag för ECMAScript), anvÀnds dekoratorer flitigt i ramverk som Angular och TypeORM, vilket fundamentalt Àndrar hur vi nÀrmar oss designmönster, metaprogrammering och aspektorienterad programmering (AOP).
Denna omfattande guide kommer att fördjupa sig i TypeScript-dekoratorer, utforska deras mekanismer, olika typer, praktiska tillÀmpningar och bÀsta praxis. Oavsett om du bygger storskaliga företagstillÀmpningar, mikroservices eller klientbaserade webbgrÀnssnitt, kommer förstÄelsen för dekoratorer att ge dig möjlighet att skriva mer deklarativ, underhÄllbar och kraftfull TypeScript-kod.
FörstÄ kÀrnkonceptet: Vad Àr en dekorator?
I grunden Àr en dekorator en speciell typ av deklaration som kan kopplas till en klassdeklaration, metod, accessor, egenskap eller parameter. Dekoratorer Àr funktioner som returnerar ett nytt vÀrde (eller modifierar ett befintligt) för det mÄl de dekorerar. Deras primÀra syfte Àr att lÀgga till metadata eller Àndra beteendet för den deklaration de Àr kopplade till, utan att direkt modifiera den underliggande kodstrukturen. Detta externa, deklarativa sÀtt att utöka kod Àr otroligt kraftfullt.
TÀnk pÄ dekoratorer som annoteringar eller etiketter som du tillÀmpar pÄ delar av din kod. Dessa etiketter kan sedan lÀsas eller ageras pÄ av andra delar av din applikation eller av ramverk, ofta vid körning, för att tillhandahÄlla ytterligare funktionalitet eller konfiguration.
Syntaxen för en dekorator
Dekoratorer prefixeras med en @-symbol, följt av dekoratorfunktionens namn. De placeras omedelbart före den deklaration de dekorerar.
@MyDecorator\nclass MyClass {\n @AnotherDecorator\n myMethod() {\n // ...\n }\n}
Aktivera dekoratorer i TypeScript
Innan du kan anvÀnda dekoratorer mÄste du aktivera kompilatoralternativet experimentalDecorators i din tsconfig.json-fil. För avancerade metaprogrammeringsfunktioner (som ofta anvÀnds av ramverk) behöver du dessutom emitDecoratorMetadata och 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 behöver Àven installera reflect-metadata:
npm install reflect-metadata --save\n# or\nyarn add reflect-metadata
Och importera den allra överst i din applikations startpunkt (t.ex. main.ts eller app.ts):
import "reflect-metadata";\n// Din applikationskod följer
Dekoratorfabriker: Anpassning inom rÀckhÄll
Ăven om en grundlĂ€ggande dekorator Ă€r en funktion, behöver du ofta skicka argument till en dekorator för att konfigurera dess beteende. Detta uppnĂ„s genom att anvĂ€nda en dekoratorfabrik. En dekoratorfabrik Ă€r en funktion som returnerar den faktiska dekoratorfunktionen. NĂ€r du tillĂ€mpar en dekoratorfabrik, anropar du den med dess argument, och den returnerar sedan den dekoratorfunktion som TypeScript tillĂ€mpar pĂ„ din kod.
Skapa ett enkelt exempel pÄ en dekoratorfabrik
LÄt oss skapa en fabrik för en Logger-dekorator som kan logga meddelanden med olika prefix.
function Logger(prefix: string) {\n return function (target: Function) {\n console.log(`[${prefix}] Klass ${target.name} har definierats.`);\n };\n}\n\n@Logger("APP_INIT")\nclass ApplicationBootstrap {\n constructor() {\n console.log("Applikationen startar...");\n }\n}\n\nconst app = new ApplicationBootstrap();\n// Utdata:\n// [APP_INIT] Klass ApplicationBootstrap har definierats.\n// Applikationen startar...
I detta exempel Àr Logger("APP_INIT") anropet till dekoratorfabriken. Det returnerar den faktiska dekoratorfunktionen som tar target: Function (klasskonstruktorn) som argument. Detta möjliggör dynamisk konfiguration av dekoratorns beteende.
Typer av dekoratorer i TypeScript
TypeScript stöder fem distinkta typer av dekoratorer, var och en tillÀmplig pÄ en specifik typ av deklaration. Signaturen för dekoratorfunktionen varierar beroende pÄ det sammanhang den tillÀmpas i.
1. Klassdekoratorer
Klassdekoratorer tillÀmpas pÄ klassdeklarationer. Dekoratorfunktionen tar klassens konstruktor som sitt enda argument. En klassdekorator kan observera, modifiera eller till och med ersÀtta en klassdefinition.
Signatur:
function ClassDecorator(target: Function) { ... }
ReturvÀrde:
Om klassdekoratorn returnerar ett vÀrde, kommer det att ersÀtta klassdeklarationen med den angivna konstruktorfunktionen. Detta Àr en kraftfull funktion, ofta anvÀnd för mixins eller klassaugmentation. Om inget vÀrde returneras, anvÀnds den ursprungliga klassen.
AnvÀndningsomrÄden:
- Registrera klasser i en beroendeinjektionsbehÄllare.
- TillÀmpa mixins eller ytterligare funktionaliteter pÄ en klass.
- Ramverksspecifika konfigurationer (t.ex. routing i ett webbramverk).
- LĂ€gga till livscykelkrokar till klasser.
Exempel pÄ klassdekorator: Injicera en tjÀnst
FörestÀll dig ett enkelt scenario för beroendeinjektion dÀr du vill markera en klass som "injicerbar" och eventuellt ange ett namn för den i en behÄllare.
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(`Registrerad tjÀnst: ${serviceName}`);\n\n // Valfritt kan du returnera en ny klass hÀr för att utöka beteendet\n return class extends constructor {\n createdAt = new Date();\n // Ytterligare egenskaper eller metoder för alla injicerade tjÀnster\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("--- TjÀnster registrerade ---");\nconsole.log(Array.from(InjectableServiceRegistry.keys()));\n\nconst userServiceConstructor = InjectableServiceRegistry.get("UserService");\nif (userServiceConstructor) {\n const userServiceInstance = new userServiceConstructor();\n console.log("AnvÀndare:", userServiceInstance.getUsers());\n // console.log("AnvÀndartjÀnst skapad den:", userServiceInstance.createdAt); // Om den returnerade klassen anvÀnds\n}
Detta exempel visar hur en klassdekorator kan registrera en klass och till och med modifiera dess konstruktor. Injectable-dekoratorn gör klassen upptÀckbar av ett teoretiskt system för beroendeinjektion.
2. Metoddekoratorer
Metoddekoratorer tillÀmpas pÄ metoddeklarationer. De tar emot tre argument: mÄlobjektet (för statiska medlemmar, konstruktorfunktionen; för instansmedlemmar, klassens prototyp), metodens namn och metodens egenskapbeskrivare.
Signatur:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
ReturvÀrde:
En metoddekorator kan returnera en ny PropertyDescriptor. Om den gör det, kommer denna beskrivare att anvÀndas för att definiera metoden. Detta gör att du kan modifiera eller ersÀtta den ursprungliga metodens implementering, vilket gör den otroligt kraftfull för AOP.
AnvÀndningsomrÄden:
- Loggning av metodanrop och deras argument/resultat.
- Cachelagring av metodresultat för att förbÀttra prestanda.
- TillÀmpa auktoriseringskontroller före metodkörning.
- MĂ€ta metodens exekveringstid.
- Debouncing eller throttling av metodanrop.
Exempel pÄ metoddekorator: Prestandaövervakning
LÄt oss skapa en MeasurePerformance-dekorator för att logga exekveringstiden för en metod.
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}\" exekverades 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 // Simulera en komplex, tidskrÀvande 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 för 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-dekoratorn omsluter den ursprungliga metoden med tidslogik och skriver ut exekveringstiden utan att belamra affÀrslogiken inuti sjÀlva metoden. Detta Àr ett klassiskt exempel pÄ aspektorienterad programmering (AOP).
3. Accessordekoratorer
Accessordekoratorer tillÀmpas pÄ accessor-deklarationer (get och set). I likhet med metoddekoratorer tar de emot mÄlobjektet, namnet pÄ accessorn och dess egenskapbeskrivare.
Signatur:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
ReturvÀrde:
En accessordekorator kan returnera en ny PropertyDescriptor, som kommer att anvÀndas för att definiera accessorn.
AnvÀndningsomrÄden:
- Validering vid instÀllning av en egenskap.
- Transformera ett vÀrde innan det sÀtts eller efter det hÀmtats.
- Kontrollera Ätkomstbehörigheter för egenskaper.
Exempel pÄ accessordekorator: Cachelagring av getters
LÄt oss skapa en dekorator som cachelagrar resultatet av en dyrbar getter-berÀkning.
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] BerÀknar vÀrde för ${String(propertyKey)}`);\n this[cacheKey] = originalGetter.apply(this);\n } else {\n console.log(`[Cache Hit] AnvÀnder cachelagrat vÀrde för ${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 // Simulerar en dyrbar berÀkning\n @CachedGetter\n get expensiveSummary(): number {\n console.log("Utför dyr sammanfattningsberÀkning...");\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örsta Ätkomsten:", generator.expensiveSummary);\nconsole.log("Andra Ätkomsten:", generator.expensiveSummary);\nconsole.log("Tredje Ätkomsten:", generator.expensiveSummary);
Denna dekorator sÀkerstÀller att berÀkningen av expensiveSummary-gettern endast körs en gÄng; efterföljande anrop returnerar det cachelagrade vÀrdet. Detta mönster Àr mycket anvÀndbart för att optimera prestanda dÀr egenskapstillgÄng innebÀr tunga berÀkningar eller externa anrop.
4. Egenskapsdekoratorer
Egenskapsdekoratorer tillÀmpas pÄ egenskapsdeklarationer. De tar emot tvÄ argument: mÄlobjektet (för statiska medlemmar, konstruktorfunktionen; för instansmedlemmar, klassens prototyp) och egenskapens namn.
Signatur:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
ReturvÀrde:
Egenskapsdekoratorer kan inte returnera nÄgot vÀrde. Deras primÀra anvÀndning Àr att registrera metadata om egenskapen. De kan inte direkt Àndra egenskapens vÀrde eller dess beskrivare vid tidpunkten för dekoreringen, eftersom beskrivaren för en egenskap Ànnu inte Àr fullt definierad nÀr egenskapsdekoratorer körs.
AnvÀndningsomrÄden:
- Registrera egenskaper för serialisering/deserialisering.
- TillÀmpa valideringsregler pÄ egenskaper.
- StÀlla in standardvÀrden eller konfigurationer för egenskaper.
- ORM (Object-Relational Mapping) kolumnmappning (t.ex.
@Column()i TypeORM).
Exempel pÄ egenskapsdekorator: Validering av obligatoriska fÀlt
LÄt oss skapa en dekorator för att markera en egenskap som "obligatorisk" och sedan validera den vid körning.
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)} Àr obligatorisk.`\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("AnvÀndare 1 valideringsfel:", validate(user1)); // []\n\nconst user2 = new UserProfile("", "Smith");\nconsole.log("AnvÀndare 2 valideringsfel:", validate(user2)); // ["firstName Àr obligatorisk."]\n\nconst user3 = new UserProfile("Alice", "");\nconsole.log("AnvÀndare 3 valideringsfel:", validate(user3)); // ["lastName Àr obligatorisk."]
Required-dekoratorn registrerar helt enkelt valideringsregeln med en central validationRules-mapp. En separat validate-funktion anvÀnder sedan denna metadata för att kontrollera instansen vid körning. Detta mönster separerar valideringslogik frÄn datadefinition, vilket gör den ÄteranvÀndbar och ren.
5. Parameterdekoratorer
Parameterdekoratorer tillÀmpas pÄ parametrar inom en klasskonstruktor eller en metod. De tar emot tre argument: mÄlobjektet (för statiska medlemmar, konstruktorfunktionen; för instansmedlemmar, klassens prototyp), metodens namn (eller undefined för konstruktorparametrar) och parameterns ordningstal i funktionens parameterlista.
Signatur:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
ReturvÀrde:
Parameterdekoratorer kan inte returnera nÄgot vÀrde. Liksom egenskapsdekoratorer Àr deras primÀra roll att lÀgga till metadata om parametern.
AnvÀndningsomrÄden:
- Registrera parametertyper för beroendeinjektion (t.ex.
@Inject()i Angular). - TillÀmpa validering eller transformering pÄ specifika parametrar.
- Extrahera metadata om API-begÀransparametrar i webbramverk.
Exempel pÄ parameterdekorator: Injicera begÀrandedata
LÄt oss simulera hur ett webbramverk kan anvÀnda parameterdekoratorer för att injicera specifik data i en metodparameter, sÄsom ett anvÀndar-ID frÄn en begÀran.
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 ramverksfunktion för att anropa en metod med lösta parametrar\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(`HÀmtar anvÀndare 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(`Tar bort anvÀndare med ID: ${userId}`);\n return { status: "deleted", id: userId };\n }\n}\n\nconst userController = new UserController();\n\n// Simulera en inkommande begÀran\nconst mockRequest = {\n id: "user123",\n token: "abc-123",\n someOtherProp: "xyz"\n};\n\nconsole.log("\\n--- Exekverar getUser ---");\nexecuteWithParams(userController, "getUser", mockRequest);\n\nconsole.log("\\n--- Exekverar deleteUser ---");\nexecuteWithParams(userController, "deleteUser", { id: "user456" });
Detta exempel visar hur parameterdekoratorer kan samla information om obligatoriska metodparametrar. Ett ramverk kan sedan anvÀnda denna insamlade metadata för att automatiskt lösa och injicera lÀmpliga vÀrden nÀr metoden anropas, vilket förenklar controller- eller tjÀnstlogiken avsevÀrt.
Dekoratorkomposition och exekveringsordning
Dekoratorer kan tillÀmpas i olika kombinationer, och att förstÄ deras exekveringsordning Àr avgörande för att förutsÀga beteende och undvika ovÀntade problem.
Flera dekoratorer pÄ ett enda mÄl
NÀr flera dekoratorer tillÀmpas pÄ en enda deklaration (t.ex. en klass, metod eller egenskap) exekveras de i en specifik ordning: frÄn botten till topp, eller frÄn höger till vÀnster, för deras utvÀrdering. DÀremot tillÀmpas deras resultat i omvÀnd ordning.
@DecoratorA\n@DecoratorB\nclass MyClass {\n // ...\n}\n
HÀr kommer DecoratorB att utvÀrderas först, sedan DecoratorA. Om de modifierar klassen (t.ex. genom att returnera en ny konstruktor), kommer modifieringen frÄn DecoratorA att omsluta eller tillÀmpas över modifieringen frÄn DecoratorB.
Exempel: Kedja metoddekoratorer
ĂvervĂ€g tvĂ„ metoddekoratorer: LogCall och Authorization.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n console.log(`[LOG] Anropar ${String(propertyKey)} med argument:`, args);\n const result = originalMethod.apply(this, args);\n console.log(`[LOG] Metoden ${String(propertyKey)} returnerade:`, 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"]; // Simulera hĂ€mtning av aktuella anvĂ€ndarroller\n const authorized = roles.some(role => currentUserRoles.includes(role));\n if (!authorized) {\n console.warn(`[AUTH] Ă
tkomst nekad för ${String(propertyKey)}. KrĂ€vs roller: ${roles.join(", ")}`);\n throw new Error("Obefogad Ă„tkomst");\n }\n console.log(`[AUTH] Ă
tkomst beviljad för ${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(`Tar bort kĂ€nslig data för ID: ${id}`);\n return `Data ID ${id} borttagen.`;\n }\n\n @Authorization(["user"])\n @LogCall // Ordningen Ă€ndrad hĂ€r\n fetchPublicData(query: string) {\n console.log(`HĂ€mtar offentlig data med frĂ„ga: ${query}`);\n return `Offentlig data för frĂ„ga: ${query}`; \n }\n}\n\nconst service = new SecureService();\n\ntry {\n console.log("\\n--- Anropar deleteSensitiveData (AdminanvĂ€ndare) ---");\n service.deleteSensitiveData("record123");\n} catch (error: any) {\n console.error(error.message);\n}\n\ntry {\n console.log("\\n--- Anropar fetchPublicData (icke-adminanvĂ€ndare) ---");\n // Simulera en icke-adminanvĂ€ndare som försöker komma Ă„t fetchPublicData som krĂ€ver 'user' roll\n const mockUserRoles = ["guest"]; // Detta kommer att misslyckas med autentisering\n // För att göra detta dynamiskt, skulle du behöva ett DI-system eller statiskt sammanhang för aktuella anvĂ€ndarroller.\n // För enkelhetens skull, antar vi att Authorization-dekoratorn har Ă„tkomst till det aktuella anvĂ€ndarsammanhanget.\n // LĂ„t oss justera Authorization-dekoratorn för att alltid anta 'admin' för demonstrationssyften, \n // sĂ„ det första anropet lyckas och det andra misslyckas för att visa olika sökvĂ€gar.\n \n // Kör igen med anvĂ€ndarroll för att fetchPublicData ska lyckas.\n // FörestĂ€ll dig att currentUserRoles i Authorization blir: ['user']\n // För detta exempel, lĂ„t oss hĂ„lla det enkelt och visa ordningseffekten.\n service.fetchPublicData("sökterm"); // Detta kommer att exekvera Auth -> Log\n} catch (error: any) {\n console.error(error.message);\n}\n\n/* FörvĂ€ntad utdata för deleteSensitiveData:\n[AUTH] Ă
tkomst beviljad för deleteSensitiveData\n[LOG] Anropar deleteSensitiveData med argument: [ 'record123' ]\nTar bort kĂ€nslig data för ID: record123\n[LOG] Metoden deleteSensitiveData returnerade: Data ID record123 borttagen.\n*/\n\n/* FörvĂ€ntad utdata för fetchPublicData (om anvĂ€ndaren har rollen 'user'):\n[LOG] Anropar fetchPublicData med argument: [ 'sökterm' ]\n[AUTH] Ă
tkomst beviljad för fetchPublicData\nHÀmtar offentlig data med frÄga: sökterm\n[LOG] Metoden fetchPublicData returnerade: Offentlig data för frÄga: sökterm\n*/
Observera ordningen: för deleteSensitiveData körs Authorization (nederst) först, sedan omsluter LogCall (överst) den. Den inre logiken för Authorization exekveras först. För fetchPublicData körs LogCall (nederst) först, sedan omslutar Authorization (överst) den. Detta innebÀr att LogCall-aspekten kommer att vara utanför Authorization-aspekten. Denna skillnad Àr avgörande för tvÀrgÄende aspekter som loggning eller felhantering, dÀr exekveringsordningen avsevÀrt kan pÄverka beteendet.
Exekveringsordning för olika mÄl
NÀr en klass, dess medlemmar och parametrar alla har dekoratorer, Àr exekveringsordningen vÀldefinierad:
- Parameterdekoratorer tillÀmpas först, för varje parameter, frÄn den sista parametern till den första.
- Sedan tillÀmpas metod-, accessor- eller egenskapsdekoratorer för varje medlem.
- Slutligen tillÀmpas klassdekoratorer pÄ sjÀlva klassen.
Inom varje kategori tillÀmpas flera dekoratorer pÄ samma mÄl frÄn botten till topp (eller frÄn höger till vÀnster).
Exempel: FullstÀndig exekveringsordning
function log(message: string) {\n return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {\n if (typeof descriptorOrIndex === 'number') {\n console.log(`Parameterdekorator: ${message} pÄ parameter #${descriptorOrIndex} av ${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(`Metod/Accessordekorator: ${message} pÄ ${String(propertyKey)}`);\n } else {\n console.log(`Egenskapsdekorator: ${message} pÄ ${String(propertyKey)}`);\n }\n } else {\n console.log(`Klassdekorator: ${message} pÄ ${target.name}`);\n }\n return descriptorOrIndex; // Returnera beskrivare för metod/accessor, undefined för andra\n };\n}\n\n@log("KlassnivÄ D")\n@log("KlassnivÄ C")\nclass MyDecoratedClass {\n @log("Statisk egenskap A")\n static staticProp: string = "";\n\n @log("Instans egenskap B")\n instanceProp: number = 0;\n\n @log("Metod D")\n @log("Metod C")\n myMethod(\n @log("Parameter Z") paramZ: string,\n @log("Parameter Y") paramY: number\n ) {\n console.log("Metod myMethod exekverades.");\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("Konstruktor exekverades.");\n }\n}\n\nnew MyDecoratedClass();\n// Anropa metod för att trigga metoddekorator\nnew MyDecoratedClass().myMethod("hello", 123);\n\n/* FörvÀntad utdataordning (ungefÀrlig, beroende pÄ specifik TypeScript-version och kompilering):\nParameterdekorator: Parameter Y pÄ parameter #1 av myMethod\nParameterdekorator: Parameter Z pÄ parameter #0 av myMethod\nEgenskapsdekorator: Statisk egenskap A pÄ staticProp\nEgenskapsdekorator: Instans egenskap B pÄ instanceProp\nMetod/Accessordekorator: Getter/Setter F pÄ myAccessor\nMetod/Accessordekorator: Metod C pÄ myMethod\nMetod/Accessordekorator: Metod D pÄ myMethod\nKlassdekorator: KlassnivÄ C pÄ MyDecoratedClass\nKlassdekorator: KlassnivÄ D pÄ MyDecoratedClass\nKonstruktor exekverades.\nMetod myMethod exekverades.\n*/
Den exakta tidpunkten för konsolloggningen kan variera nÄgot beroende pÄ nÀr en konstruktor eller metod anropas, men ordningen i vilken dekoratorfunktionerna sjÀlva exekveras (och dÀrmed deras sidoeffekter eller returnerade vÀrden tillÀmpas) följer reglerna ovan.
Praktiska tillÀmpningar och designmönster med dekoratorer
Dekoratorer, sÀrskilt i kombination med reflect-metadata polyfill, öppnar upp ett nytt omrÄde för metadatadriven programmering. Detta möjliggör kraftfulla designmönster som abstraherar bort boilerplate och tvÀrgÄende aspekter.
1. Beroendeinjektion (DI)
En av de mest framtrÀdande anvÀndningarna av dekoratorer Àr i beroendeinjektionsramverk (som Angulars @Injectable(), @Component(), etc., eller NestJS omfattande anvÀndning av DI). Dekoratorer lÄter dig deklarera beroenden direkt pÄ konstruktorer eller egenskaper, vilket gör att ramverket automatiskt kan instansiera och tillhandahÄlla de korrekta tjÀnsterna.
Exempel: Förenklad tjÀnsteinjektion
import "reflect-metadata"; // Viktigt för 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} Àr inte markerad som @Injectable.`);\n }\n\n // HÀmta konstruktorparametertyper (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 // AnvÀnd explicit @Inject-token om tillhandahÄllen, annars hÀrled typ\n const token = explicitInjections[index] || paramType;\n if (token === undefined) {\n throw new Error(`Kan inte lösa parameter pÄ index ${index} för ${target.name}. Det kan vara ett cirkulÀrt beroende eller primitiv typ utan explicit @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// Definiera tjÀnster\n@Injectable()\nclass DatabaseService {\n connect() {\n console.log("Ansluter till databas...");\n return "DB-anslutning";\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: Autentiserar med ${this.db.connect()}`);\n return "AnvÀndare inloggad";\n }\n}\n\n@Injectable()\nclass UserService {\n private authService: AuthService;\n private dbService: DatabaseService; // Exempel pÄ injektion via egenskap med en anpassad dekorator eller ramverksfunktion\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: HÀmtar anvÀndarprofil...");\n return { id: 1, name: "Global AnvÀndare" };\n }\n}\n\n// Lös huvudtjÀnsten\nconsole.log("--- Löser UserService ---");\nconst userService = Container.resolve(UserService);\nconsole.log(userService.getUserProfile());\n\nconsole.log("\\n--- Löser AuthService (bör vara cachelagrad) ---");\nconst authService = Container.resolve(AuthService);\nauthService.login();
Detta utförliga exempel visar hur @Injectable- och @Inject-dekoratorer, kombinerade med reflect-metadata, tillÄter en anpassad Container att automatiskt lösa och tillhandahÄlla beroenden. Metadatan design:paramtypes som automatiskt emitteras av TypeScript (nÀr emitDecoratorMetadata Àr sant) Àr avgörande hÀr.
2. Aspektorienterad programmering (AOP)
AOP fokuserar pÄ att modularisera tvÀrgÄende aspekter (t.ex. loggning, sÀkerhet, transaktioner) som korsar flera klasser och moduler. Dekoratorer passar utmÀrkt för att implementera AOP-koncept i TypeScript.
Exempel: Loggning med metoddekorator
Om vi ÄtergÄr till LogCall-dekoratorn Àr den ett perfekt exempel pÄ AOP. Den lÀgger till loggningsbeteende till vilken metod som helst utan att modifiera metodens ursprungliga kod. Detta separerar "vad man ska göra" (affÀrslogik) frÄn "hur man gör det" (loggning, prestandaövervakning, etc.).
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 in i metod: ${String(propertyKey)} med argument:`, args);\n try {\n const result = originalMethod.apply(this, args);\n console.log(`[LOG AOP] GĂ„r ut ur metod: ${String(propertyKey)} med resultat:`, result);\n return result;\n } catch (error: any) {\n console.error(`[LOG AOP] Fel i metod ${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("Betalningsbeloppet mĂ„ste vara positivt.");\n }\n console.log(`Bearbetar betalning av ${amount} ${currency}...`);\n return `Betalning av ${amount} ${currency} bearbetades framgĂ„ngsrikt.`;\n }\n\n @LogMethod\n refundPayment(transactionId: string) {\n console.log(`Ă
terbetalar betalning för transaktions-ID: ${transactionId}...`);\n return `Ă
terbetalning initierad för ${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("FÄngade fel:", error.message);\n}
Detta tillvÀgagÄngssÀtt hÄller PaymentProcessor-klassen fokuserad enbart pÄ betalningslogik, medan LogMethod-dekoratorn hanterar den tvÀrgÄende aspekten av loggning.
3. Validering och transformering
Dekoratorer Àr otroligt anvÀndbara för att definiera valideringsregler direkt pÄ egenskaper eller för att transformera data under serialisering/deserialisering.
Exempel: Datavalidering med egenskapsdekoratorer
Exemplet @Required tidigare visade redan detta. HÀr Àr ett annat exempel med en numerisk intervallvalidering.
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)} mÄste vara ett positivt nummer.`);\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)} fÄr vara högst ${maxLength} tecken lÄng.`);\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 felmeddelanden:", Product.validate(product1)); // []\n\nconst product2 = new Product("Very long product name that exceeds fifty characters limit for testing purpose", 50);\nconsole.log("Produkt 2 felmeddelanden:", Product.validate(product2)); // ["name fÄr vara högst 50 tecken lÄng."]\n\nconst product3 = new Product("Book", -10);\nconsole.log("Produkt 3 felmeddelanden:", Product.validate(product3)); // ["price mÄste vara ett positivt nummer."]
Denna instÀllning lÄter dig deklarativt definiera valideringsregler för dina modellegenskaper, vilket gör dina datamodeller sjÀlvbeskrivande nÀr det gÀller deras begrÀnsningar.
BÀsta praxis och övervÀganden
Ăven om dekoratorer Ă€r kraftfulla bör de anvĂ€ndas med omdöme. Felaktig anvĂ€ndning kan leda till kod som Ă€r svĂ„rare att felsöka eller förstĂ„.
NÀr du ska anvÀnda dekoratorer (och nÀr du inte ska)
- AnvÀnd dem för:
- TvÀrgÄende aspekter: Loggning, cachelagring, auktorisering, transaktionshantering.
- Metadatadeklaration: Definiera schema för ORM:er, valideringsregler, DI-konfiguration.
- Ramverksintegration: NÀr du bygger eller anvÀnder ramverk som utnyttjar metadata.
- Minska boilerplate: Abstrahera repetitiva kodmönster.
- Undvik dem för:
- Enkla funktionsanrop: Om ett vanligt funktionsanrop kan uppnÄ samma resultat tydligt, föredra det.
- AffÀrslogik: Dekoratorer ska utöka, inte definiera, kÀrnaffÀrslogik.
- Ăverkomplicering: Om anvĂ€ndning av en dekorator gör koden mindre lĂ€sbar eller svĂ„rare att testa, övervĂ€g igen.
Prestandakonsekvenser
Dekoratorer exekveras vid kompileringstid (eller definitionstid i JavaScript-körtiden om transpilerad). Transformationen eller metadatasamlingen sker nÀr klassen/metoden definieras, inte vid varje anrop. DÀrför Àr prestandapÄverkan vid körning av *tillÀmpning* av dekoratorer minimal. Dock kan *logiken inuti* dina dekoratorer ha en prestandapÄverkan, sÀrskilt om de utför dyra operationer vid varje metodanrop (t.ex. komplexa berÀkningar inom en metoddekorator).
UnderhÄllbarhet och lÀsbarhet
Dekoratorer, nÀr de anvÀnds korrekt, kan avsevÀrt förbÀttra lÀsbarheten genom att flytta boilerplate-kod bort frÄn huvudlogiken. Men om de utför komplexa, dolda transformationer kan felsökning bli utmanande. Se till att dina dekoratorer Àr vÀldokumenterade och att deras beteende Àr förutsÀgbart.
Experimentell status och framtid för dekoratorer
Det Àr viktigt att upprepa att TypeScript-dekoratorer baseras pÄ ett TC39-förslag i Stage 3. Detta innebÀr att specifikationen i stort sett Àr stabil men fortfarande kan genomgÄ mindre Àndringar innan den blir en del av den officiella ECMAScript-standarden. Ramverk som Angular har anammat dem och satsar pÄ deras eventuella standardisering. Detta innebÀr en viss risk, Àven om betydande brytande Àndringar, med tanke pÄ deras utbredda anvÀndning, Àr osannolika.
TC39-förslaget har utvecklats. TypeScript's nuvarande implementering baseras pÄ en Àldre version av förslaget. Det finns en distinktion mellan "Legacy Decorators" och "Standard Decorators". NÀr den officiella standarden landar kommer TypeScript sannolikt att uppdatera sin implementering. För de flesta utvecklare som anvÀnder ramverk kommer denna övergÄng att hanteras av ramverket sjÀlvt. För biblioteksförfattare kan det bli nödvÀndigt att förstÄ de subtila skillnaderna mellan Àldre och framtida standarddekoratorer.
Kompilatoralternativet emitDecoratorMetadata
Detta alternativ, nÀr det Àr instÀllt pÄ true i tsconfig.json, instruerar TypeScript-kompilatorn att emittera viss designtids-typmetadata till den kompilerade JavaScript-koden. Denna metadata inkluderar typen av konstruktorparametrarna (design:paramtypes), returtypen för metoder (design:returntype) och typen av egenskaper (design:type).
Denna emitterade metadata Àr inte en del av standard JavaScript-körtiden. Den konsumeras typiskt av reflect-metadata polyfill, som sedan gör den tillgÀnglig via Reflect.getMetadata()-funktionerna. Detta Àr absolut avgörande för avancerade mönster som beroendeinjektion, dÀr en behÄllare behöver kÀnna till typerna av beroenden en klass krÀver utan explicit konfiguration.
Avancerade mönster med dekoratorer
Dekoratorer kan kombineras och utökas för att bygga Ànnu mer sofistikerade mönster.
1. Dekorera dekoratorer (Higher-Order Decorators)
Du kan skapa dekoratorer som modifierar eller komponerar andra dekoratorer. Detta Àr mindre vanligt men demonstrerar dekoratorernas funktionella natur.
// En dekorator som sĂ€kerstĂ€ller att en metod loggas och Ă€ven krĂ€ver administratörsroller\nfunction AdminAndLoggedMethod() {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n // TillĂ€mpa auktorisering först (inre)\n Authorization(["admin"])(target, propertyKey, descriptor);\n // TillĂ€mpa sedan LogCall (yttre)\n LogCall(target, propertyKey, descriptor);\n\n return descriptor; // Returnera den modifierade beskrivaren\n };\n}\n\nclass AdminPanel {\n @AdminAndLoggedMethod()\n deleteUserAccount(userId: string) {\n console.log(`Tar bort anvĂ€ndarkonto: ${userId}`);\n return `AnvĂ€ndare ${userId} borttagen.`;\n }\n}\n\nconst adminPanel = new AdminPanel();\nadminPanel.deleteUserAccount("user007");\n/* FörvĂ€ntad utdata (förutsatt admin-roll):\n[AUTH] Ă
tkomst beviljad för deleteUserAccount\n[LOG] Anropar deleteUserAccount med argument: [ 'user007' ]\nTar bort anvÀndarkonto: user007\n[LOG] Metoden deleteUserAccount returnerade: AnvÀndare user007 borttagen.\n*/
HÀr Àr AdminAndLoggedMethod en fabrik som returnerar en dekorator, och inuti den dekoratorn tillÀmpar den tvÄ andra dekoratorer. Detta mönster kan kapsla in komplexa dekoratorkompositioner.
2. AnvÀnda dekoratorer för Mixins
Medan TypeScript erbjuder andra sÀtt att implementera mixins, kan dekoratorer anvÀndas för att injicera kapabiliteter i klasser pÄ ett deklarativt sÀtt.
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 bortskaffat.");\n }\n}\n\nclass Loggable {\n log(message: string) {\n console.log(`[Loggbar] ${message}`);\n }\n}\n\n@ApplyMixins([Disposable, Loggable])\nclass MyResource implements Disposable, Loggable {\n // Dessa egenskaper/metoder injiceras av dekoratorn\n isDisposed!: boolean;\n dispose!: () => void;\n log!: (message: string) => void;\n\n constructor(public name: string) {\n this.log(`Resurs ${this.name} skapades.`);\n }\n\n cleanUp() {\n this.dispose();\n this.log(`Resurs ${this.name} stĂ€dades.`);\n }\n}\n\nconst resource = new MyResource("NetworkConnection");\nconsole.log(`Ăr bortskaffad: ${resource.isDisposed}`);\nresource.cleanUp();\nconsole.log(`Ăr bortskaffad: ${resource.isDisposed}`);
Denna @ApplyMixins-dekorator kopierar dynamiskt metoder och egenskaper frÄn baskonstruktorer till den hÀrledda klassens prototyp, vilket effektivt "mixar in" funktionaliteter.
Slutsats: StÀrka modern TypeScript-utveckling
TypeScript-dekoratorer Àr en kraftfull och uttrycksfull funktion som möjliggör ett nytt paradigm för metadatadriven och aspektorienterad programmering. De tillÄter utvecklare att förbÀttra, modifiera och lÀgga till deklarativa beteenden till klasser, metoder, egenskaper, accessorer och parametrar utan att Àndra deras kÀrnlogik. Denna separation av ansvar leder till renare, mer underhÄllbar och mycket ÄteranvÀndbar kod.
FrĂ„n att förenkla beroendeinjektion och implementera robusta valideringssystem till att lĂ€gga till tvĂ€rgĂ„ende aspekter som loggning och prestandaövervakning, erbjuder dekoratorer en elegant lösning pĂ„ mĂ„nga vanliga utvecklingsutmaningar. Ăven om deras experimentella status krĂ€ver medvetenhet, signalerar deras utbredda anvĂ€ndning i stora ramverk deras praktiska vĂ€rde och framtida relevans.
Genom att bemÀstra TypeScript-dekoratorer fÄr du ett betydande verktyg i din arsenal, vilket gör att du kan bygga mer robusta, skalbara och intelligenta applikationer. Anamma dem ansvarsfullt, förstÄ deras mekanik och lÄs upp en ny nivÄ av deklarativ kraft i dina TypeScript-projekt.