Dansk

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:

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:

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:

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:

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:

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:

  1. Parameter-decorators anvendes først, for hver parameter, startende fra den sidste parameter til den første.
  2. Derefter anvendes Metode-, Accessor- eller Property-decorators for hvert medlem.
  3. 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)

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.