Svenska

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:

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:

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:

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:

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:

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:

  1. Parameterdekoratorer tillämpas först, för varje parameter, från den sista parametern till den första.
  2. Sedan tillämpas metod-, accessor- eller egenskapsdekoratorer för varje medlem.
  3. 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)

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.