Utforsk kraften i TypeScript Decorators for metadataprogrammering, aspektorientert programmering og forbedring av kode med deklarative mønstre. En omfattende guide for globale utviklere.
TypeScript Decorators: Mestring av Metadataprogrammeringsmønstre for Robuste Applikasjoner
I det enorme landskapet av moderne programvareutvikling er det avgjørende å opprettholde rene, skalerbare og håndterbare kodebaser. TypeScript, med sitt kraftige typesystem og avanserte funksjoner, gir utviklere verktøyene for å oppnå dette. Blant de mest spennende og transformative funksjonene er Dekoratører. Selv om det på skrivende tidspunkt fortsatt er en eksperimentell funksjon (Stage 3-forslag for ECMAScript), er dekoratører mye brukt i rammeverk som Angular og TypeORM, og endrer fundamentalt hvordan vi tilnærmer oss designmønstre, metadataprogrammering og aspektorientert programmering (AOP).
Denne omfattende guiden vil dykke dypt inn i TypeScript-dekoratører, utforske deres mekanismer, ulike typer, praktiske anvendelser og beste praksis. Enten du bygger storskala bedriftsapplikasjoner, mikrotjenester eller klient-side webgrensesnitt, vil forståelsen av dekoratører gi deg muligheten til å skrive mer deklarativ, vedlikeholdbar og kraftig TypeScript-kode.
Forstå Kjernekonseptet: Hva er en Dekoratør?
I sin kjerne er en dekoratør en spesiell type deklarasjon som kan festes til en klassedeklarasjon, metode, aksessor, egenskap eller parameter. Dekoratører er funksjoner som returnerer en ny verdi (eller endrer en eksisterende) for målet de dekorerer. Deres primære formål er å legge til metadata eller endre oppførselen til deklarasjonen de er festet til, uten å endre den underliggende kodestrukturen direkte. Denne eksterne, deklarative måten å utvide kode på er utrolig kraftig.
Tenk på dekoratører som annotasjoner eller etiketter du bruker på deler av koden din. Disse etikettene kan deretter leses eller handles på av andre deler av applikasjonen din eller av rammeverk, ofte under kjøring, for å gi ekstra funksjonalitet eller konfigurasjon.
Syntaksen til en Dekoratør
Dekoratører har et @
-symbol som prefiks, etterfulgt av dekoratørfunksjonens navn. De plasseres umiddelbart før deklarasjonen de dekorerer.
@MyDecorator
class MyClass {
@AnotherDecorator
myMethod() {
// ...
}
}
Aktivering av Dekoratører i TypeScript
Før du kan bruke dekoratører, må du aktivere kompileringsalternativet experimentalDecorators
i din tsconfig.json
-fil. I tillegg, for avanserte metadatarefleksjonsegenskaper (ofte brukt av rammeverk), trenger du også emitDecoratorMetadata
og reflect-metadata
-polyfillen.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Du må også installere reflect-metadata
:
npm install reflect-metadata --save
# eller
yarn add reflect-metadata
Og importere det helt øverst i applikasjonens inngangspunkt (f.eks. main.ts
eller app.ts
):
import "reflect-metadata";
// Din applikasjonskode følger
Dekoratørfabrikker: Tilpasning til Fingerspissene
Selv om en grunnleggende dekoratør er en funksjon, vil du ofte trenge å sende argumenter til en dekoratør for å konfigurere oppførselen. Dette oppnås ved å bruke en dekoratørfabrikk. En dekoratørfabrikk er en funksjon som returnerer den faktiske dekoratørfunksjonen. Når du bruker en dekoratørfabrikk, kaller du den med argumentene, og den returnerer deretter dekoratørfunksjonen som TypeScript bruker på koden din.
Eksempel på en Enkel Dekoratørfabrikk
La oss lage en fabrikk for en Logger
-dekoratør som kan logge meldinger med forskjellige prefikser.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] Klassen ${target.name} er definert.`);
};
}
@Logger("APP_INIT")
class ApplicationBootstrap {
constructor() {
console.log("Applikasjonen starter...");
}
}
const app = new ApplicationBootstrap();
// Utdata:
// [APP_INIT] Klassen ApplicationBootstrap er definert.
// Applikasjonen starter...
I dette eksempelet er Logger("APP_INIT")
kallet til dekoratørfabrikken. Det returnerer den faktiske dekoratørfunksjonen som tar target: Function
(klassekonstruktøren) som sitt argument. Dette tillater dynamisk konfigurasjon av dekoratørens oppførsel.
Typer Dekoratører i TypeScript
TypeScript støtter fem distinkte typer dekoratører, hver anvendelig for en spesifikk type deklarasjon. Signaturen til dekoratørfunksjonen varierer basert på konteksten den brukes i.
1. Klassedekoratører
Klassedekoratører brukes på klassedeklarasjoner. Dekoratørfunksjonen mottar klassens konstruktør som sitt eneste argument. En klassedekoratør kan observere, modifisere eller til og med erstatte en klassedefinisjon.
Signatur:
function ClassDecorator(target: Function) { ... }
Returverdi:
Hvis klassedekoratøren returnerer en verdi, vil den erstatte klassedeklarasjonen med den angitte konstruktørfunksjonen. Dette er en kraftig funksjon, ofte brukt for mixins eller klasseutvidelse. Hvis ingen verdi returneres, brukes den opprinnelige klassen.
Bruksområder:
- Registrere klasser i en dependency injection-container.
- Anvende mixins eller tilleggsfunksjonaliteter til en klasse.
- Rammeverk-spesifikke konfigurasjoner (f.eks. ruting i et web-rammeverk).
- Legge til livssykluskroker (lifecycle hooks) til klasser.
Eksempel på Klassedekoratør: Injeksjon av en Tjeneste
Se for deg et enkelt scenario for dependency injection der du vil merke en klasse som "injectable" og eventuelt gi den et navn i en container.
const InjectableServiceRegistry = new Map<string, Function>();
function Injectable(name?: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
const serviceName = name || constructor.name;
InjectableServiceRegistry.set(serviceName, constructor);
console.log(`Registrert tjeneste: ${serviceName}`);
// Eventuelt kan du returnere en ny klasse her for å utvide oppførselen
return class extends constructor {
createdAt = new Date();
// Ytterligere egenskaper eller metoder for alle injiserte tjenester
};
};
}
@Injectable("UserService")
class UserDataService {
getUsers() {
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
}
}
@Injectable()
class ProductDataService {
getProducts() {
return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
}
}
console.log("--- Tjenester Registrert ---");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log("Brukere:", userServiceInstance.getUsers());
// console.log("User Service Created At:", userServiceInstance.createdAt); // Hvis den returnerte klassen brukes
}
Dette eksempelet demonstrerer hvordan en klassedekoratør kan registrere en klasse og til og med modifisere dens konstruktør. Injectable
-dekoratøren gjør klassen oppdagbar for et teoretisk dependency injection-system.
2. Metodedekoratører
Metodedekoratører brukes på metodedeklarasjoner. De mottar tre argumenter: målobjektet (for statiske medlemmer, konstruktørfunksjonen; for instansmedlemmer, klassens prototype), navnet på metoden, og egenskapbeskrivelsen (property descriptor) til metoden.
Signatur:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Returverdi:
En metodedekoratør kan returnere en ny PropertyDescriptor
. Hvis den gjør det, vil denne beskrivelsen bli brukt for å definere metoden. Dette lar deg modifisere eller erstatte den opprinnelige metodens implementasjon, noe som gjør den utrolig kraftig for AOP.
Bruksområder:
- Logging av metodekall og deres argumenter/resultater.
- Mellomlagring (caching) av metoderesultater for å forbedre ytelsen.
- Anvende autorisasjonssjekker før metodeutførelse.
- Måle metodeutførelsestid.
- Debouncing eller throttling av metodekall.
Eksempel på Metodedekoratør: Ytelsesovervåking
La oss lage en MeasurePerformance
-dekoratør for å logge utførelsestiden til en metode.
function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = process.hrtime.bigint();
const result = originalMethod.apply(this, args);
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1_000_000;
console.log(`Metoden "${propertyKey}" ble utført på ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Simulerer en kompleks, tidkrevende operasjon
for (let i = 0; i < 1_000_000; i++) {
Math.sin(i);
}
return data.map(n => n * 2);
}
@MeasurePerformance
fetchRemoteData(id: string): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Data for ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));
MeasurePerformance
-dekoratøren pakker inn den opprinnelige metoden med tidsmålingslogikk, og skriver ut utførelsestiden uten å rote til forretningslogikken i selve metoden. Dette er et klassisk eksempel på Aspektorientert Programmering (AOP).
3. Aksessordekoratører
Aksessordekoratører brukes på aksessor- (get
og set
) deklarasjoner. I likhet med metodedekoratører, mottar de målobjektet, navnet på aksessoren, og dens egenskapbeskrivelse.
Signatur:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Returverdi:
En aksessordekoratør kan returnere en ny PropertyDescriptor
, som vil bli brukt til å definere aksessoren.
Bruksområder:
- Validering ved setting av en egenskap.
- Transformering av en verdi før den settes eller etter at den er hentet.
- Kontrollere tilgangsrettigheter for egenskaper.
Eksempel på Aksessordekoratør: Mellomlagring av Gettere
La oss lage en dekoratør som mellomlagrer resultatet av en kostbar getter-beregning.
function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGetter = descriptor.get;
const cacheKey = `_cached_${String(propertyKey)}`;
if (originalGetter) {
descriptor.get = function() {
if (this[cacheKey] === undefined) {
console.log(`[Cache Miss] Beregner verdi for ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Bruker mellomlagret verdi for ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Simulerer en kostbar beregning
@CachedGetter
get expensiveSummary(): number {
console.log("Utfører kostbar oppsummeringsberegning...");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log("Første tilgang:", generator.expensiveSummary);
console.log("Andre tilgang:", generator.expensiveSummary);
console.log("Tredje tilgang:", generator.expensiveSummary);
Denne dekoratøren sikrer at beregningen til expensiveSummary
-getteren bare kjøres én gang, påfølgende kall returnerer den mellomlagrede verdien. Dette mønsteret er svært nyttig for å optimalisere ytelsen der tilgang til egenskaper innebærer tunge beregninger eller eksterne kall.
4. Egenskapsdekoratører
Egenskapsdekoratører brukes på egenskapsdeklarasjoner. De mottar to argumenter: målobjektet (for statiske medlemmer, konstruktørfunksjonen; for instansmedlemmer, klassens prototype), og navnet på egenskapen.
Signatur:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
Returverdi:
Egenskapsdekoratører kan ikke returnere noen verdi. Deres primære bruk er å registrere metadata om egenskapen. De kan ikke direkte endre egenskapens verdi eller dens beskrivelse på dekorasjonstidspunktet, da beskrivelsen for en egenskap ennå ikke er fullt definert når egenskapsdekoratører kjøres.
Bruksområder:
- Registrere egenskaper for serialisering/deserialisering.
- Anvende valideringsregler på egenskaper.
- Sette standardverdier eller konfigurasjoner for egenskaper.
- ORM (Object-Relational Mapping) kolonnemapping (f.eks.
@Column()
i TypeORM).
Eksempel på Egenskapsdekoratør: Validering av Obligatorisk Felt
La oss lage en dekoratør for å merke en egenskap som "obligatorisk" og deretter validere den under kjøring.
interface ValidationRule {
property: string | symbol;
validate: (value: any) => boolean;
message: string;
}
const validationRules: Map<Function, ValidationRule[]> = new Map();
function Required(target: Object, propertyKey: string | symbol) {
const rules = validationRules.get(target.constructor) || [];
rules.push({
property: propertyKey,
validate: (value: any) => value !== null && value !== undefined && value !== "",
message: `${String(propertyKey)} er påkrevd.`
});
validationRules.set(target.constructor, rules);
}
function validate(instance: any): string[] {
const classRules = validationRules.get(instance.constructor) || [];
const errors: string[] = [];
for (const rule of classRules) {
if (!rule.validate(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
class UserProfile {
@Required
firstName: string;
@Required
lastName: string;
age?: number;
constructor(firstName: string, lastName: string, age?: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
const user1 = new UserProfile("John", "Doe", 30);
console.log("Valideringsfeil for bruker 1:", validate(user1)); // []
const user2 = new UserProfile("", "Smith");
console.log("Valideringsfeil for bruker 2:", validate(user2)); // ["firstName er påkrevd."]
const user3 = new UserProfile("Alice", "");
console.log("Valideringsfeil for bruker 3:", validate(user3)); // ["lastName er påkrevd."]
Required
-dekoratøren registrerer simpelthen valideringsregelen i et sentralt validationRules
-map. En separat validate
-funksjon bruker deretter denne metadataen til å sjekke instansen under kjøring. Dette mønsteret separerer valideringslogikk fra datadefinisjon, noe som gjør den gjenbrukbar og ren.
5. Parameterdekoratører
Parameterdekoratører brukes på parametere i en klassekonstruktør eller en metode. De mottar tre argumenter: målobjektet (for statiske medlemmer, konstruktørfunksjonen; for instansmedlemmer, klassens prototype), navnet på metoden (eller undefined
for konstruktørparametere), og den ordinale indeksen til parameteren i funksjonens parameterliste.
Signatur:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Returverdi:
Parameterdekoratører kan ikke returnere noen verdi. Som egenskapsdekoratører, er deres primære rolle å legge til metadata om parameteren.
Bruksområder:
- Registrere parametertyper for dependency injection (f.eks.
@Inject()
i Angular). - Anvende validering eller transformasjon på spesifikke parametere.
- Hente ut metadata om API-forespørselsparametere i web-rammeverk.
Eksempel på Parameterdekoratør: Injeksjon av Forespørselsdata
La oss simulere hvordan et web-rammeverk kan bruke parameterdekoratører til å injisere spesifikke data i en metodeparameter, som for eksempel en bruker-ID fra en forespørsel.
interface ParameterMetadata {
index: number;
key: string | symbol;
resolver: (request: any) => any;
}
const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();
function RequestParam(paramName: string) {
return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
const targetKey = propertyKey || "constructor";
let methodResolvers = parameterResolvers.get(target.constructor);
if (!methodResolvers) {
methodResolvers = new Map();
parameterResolvers.set(target.constructor, methodResolvers);
}
const paramMetadata = methodResolvers.get(targetKey) || [];
paramMetadata.push({
index: parameterIndex,
key: targetKey,
resolver: (request: any) => request[paramName]
});
methodResolvers.set(targetKey, paramMetadata);
};
}
// En hypotetisk rammeverkfunksjon for å kalle en metode med oppløste parametere
function executeWithParams(instance: any, methodName: string, request: any) {
const classResolvers = parameterResolvers.get(instance.constructor);
if (!classResolvers) {
return (instance[methodName] as Function).apply(instance, []);
}
const methodParamMetadata = classResolvers.get(methodName);
if (!methodParamMetadata) {
return (instance[methodName] as Function).apply(instance, []);
}
const args: any[] = Array(methodParamMetadata.length);
for (const meta of methodParamMetadata) {
args[meta.index] = meta.resolver(request);
}
return (instance[methodName] as Function).apply(instance, args);
}
class UserController {
getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
console.log(`Henter bruker med ID: ${userId}, Token: ${authToken || "N/A"}`);
return { id: userId, name: "Jane Doe" };
}
deleteUser(@RequestParam("id") userId: string) {
console.log(`Sletter bruker med ID: ${userId}`);
return { status: "deleted", id: userId };
}
}
const userController = new UserController();
// Simulerer en innkommende forespørsel
const mockRequest = {
id: "user123",
token: "abc-123",
someOtherProp: "xyz"
};
console.log("\n--- Utfører getUser ---");
executeWithParams(userController, "getUser", mockRequest);
console.log("\n--- Utfører deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });
Dette eksempelet viser hvordan parameterdekoratører kan samle informasjon om nødvendige metodeparametere. Et rammeverk kan deretter bruke denne innsamlede metadataen til automatisk å løse opp og injisere passende verdier når metoden kalles, noe som forenkler kontroller- eller tjenestelogikk betydelig.
Komposisjon og Utførelsesrekkefølge for Dekoratører
Dekoratører kan anvendes i ulike kombinasjoner, og det er avgjørende å forstå deres utførelsesrekkefølge for å forutsi atferd og unngå uventede problemer.
Flere Dekoratører på ett Enkelt Mål
Når flere dekoratører brukes på en enkelt deklarasjon (f.eks. en klasse, metode eller egenskap), utføres de i en bestemt rekkefølge for evalueringen sin: fra bunn til topp, eller fra høyre til venstre. Resultatene deres blir imidlertid anvendt i motsatt rekkefølge.
@DecoratorA
@DecoratorB
class MyClass {
// ...
}
Her vil DecoratorB
bli evaluert først, deretter DecoratorA
. Hvis de modifiserer klassen (f.eks. ved å returnere en ny konstruktør), vil modifikasjonen fra DecoratorA
pakke seg rundt eller anvendes over modifikasjonen fra DecoratorB
.
Eksempel: Kjede Metodedekoratører
Tenk på to metodedekoratører: LogCall
og Authorization
.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Kaller ${String(propertyKey)} med argumenter:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] Metode ${String(propertyKey)} returnerte:`, result);
return result;
};
return descriptor;
}
function Authorization(roles: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUserRoles = ["admin"]; // Simulerer henting av nåværende brukerroller
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Tilgang nektet for ${String(propertyKey)}. Krevde roller: ${roles.join(", ")}`);
throw new Error("Uautorisert tilgang");
}
console.log(`[AUTH] Tilgang gitt for ${String(propertyKey)}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization(["admin"])
deleteSensitiveData(id: string) {
console.log(`Sletter sensitive data for ID: ${id}`);
return `Data ID ${id} slettet.`;
}
@Authorization(["user"])
@LogCall // Rekkefølgen endret her
fetchPublicData(query: string) {
console.log(`Henter offentlige data med søk: ${query}`);
return `Offentlige data for søk: ${query}`;
}
}
const service = new SecureService();
try {
console.log("\n--- Kaller deleteSensitiveData (Admin-bruker) ---");
service.deleteSensitiveData("record123");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("\n--- Kaller fetchPublicData (Ikke-admin-bruker) ---");
// Simulerer en ikke-admin-bruker som prøver å få tilgang til fetchPublicData som krever 'user'-rollen
const mockUserRoles = ["guest"]; // Dette vil mislykkes autorisasjon
// For å gjøre dette dynamisk, trenger du et DI-system eller statisk kontekst for nåværende brukerroller.
// For enkelhets skyld antar vi at Authorization-dekoratøren har tilgang til nåværende brukerkontekst.
// La oss justere Authorization-dekoratøren til å alltid anta 'admin' for demoformål,
// slik at det første kallet lykkes og det andre mislykkes for å vise forskjellige veier.
// Kjør på nytt med brukerrolle for at fetchPublicData skal lykkes.
// Tenk deg at currentUserRoles i Authorization blir: ['user']
// For dette eksempelet, la oss holde det enkelt og vise rekkefølgeeffekten.
service.fetchPublicData("søkeord"); // Dette vil utføre Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Forventet utdata for deleteSensitiveData:
[AUTH] Tilgang gitt for deleteSensitiveData
[LOG] Kaller deleteSensitiveData med argumenter: [ 'record123' ]
Sletter sensitive data for ID: record123
[LOG] Metode deleteSensitiveData returnerte: Data ID record123 slettet.
*/
/* Forventet utdata for fetchPublicData (hvis bruker har 'user'-rollen):
[LOG] Kaller fetchPublicData med argumenter: [ 'søkeord' ]
[AUTH] Tilgang gitt for fetchPublicData
Henter offentlige data med søk: søkeord
[LOG] Metode fetchPublicData returnerte: Offentlige data for søk: søkeord
*/
Legg merke til rekkefølgen: for deleteSensitiveData
kjøres Authorization
(nederst) først, deretter pakker LogCall
(øverst) seg rundt den. Den indre logikken til Authorization
utføres først. For fetchPublicData
kjøres LogCall
(nederst) først, deretter pakker Authorization
(øverst) seg rundt den. Dette betyr at LogCall
-aspektet vil være utenfor Authorization
-aspektet. Denne forskjellen er kritisk for tverrgående anliggender som logging eller feilhåndtering, der rekkefølgen av utførelse kan ha betydelig innvirkning på atferden.
Utførelsesrekkefølge for Forskjellige Mål
Når en klasse, dens medlemmer og parametere alle har dekoratører, er utførelsesrekkefølgen veldefinert:
- Parameterdekoratører anvendes først, for hver parameter, fra siste parameter til den første.
- Deretter anvendes Metode-, Aksessor- eller Egenskapsdekoratører for hvert medlem.
- Til slutt anvendes Klassedekoratører på selve klassen.
Innenfor hver kategori anvendes flere dekoratører på samme mål fra bunn til topp (eller fra høyre til venstre).
Eksempel: Full Utførelsesrekkefølge
function log(message: string) {
return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
if (typeof descriptorOrIndex === 'number') {
console.log(`Param-dekoratør: ${message} på parameter #${descriptorOrIndex} av ${String(propertyKey || "constructor")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Metode/Aksessor-dekoratør: ${message} på ${String(propertyKey)}`);
} else {
console.log(`Egenskapsdekoratør: ${message} på ${String(propertyKey)}`);
}
} else {
console.log(`Klassedekoratør: ${message} på ${target.name}`);
}
return descriptorOrIndex; // Returner descriptor for metode/aksessor, undefined for andre
};
}
@log("Klassenivå D")
@log("Klassenivå C")
class MyDecoratedClass {
@log("Statisk Egenskap A")
static staticProp: string = "";
@log("Instanse-egenskap B")
instanceProp: number = 0;
@log("Metode D")
@log("Metode C")
myMethod(
@log("Parameter Z") paramZ: string,
@log("Parameter Y") paramY: number
) {
console.log("Metoden myMethod utført.");
}
@log("Getter/Setter F")
get myAccessor() {
return "";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log("Konstruktør utført.");
}
}
new MyDecoratedClass();
// Kall metoden for å utløse metodedekoratøren
new MyDecoratedClass().myMethod("hello", 123);
/* Forutsagt utdatarekkefølge (omtrentlig, avhengig av spesifikk TypeScript-versjon og kompilering):
Param-dekoratør: Parameter Y på parameter #1 av myMethod
Param-dekoratør: Parameter Z på parameter #0 av myMethod
Egenskapsdekoratør: Statisk Egenskap A på staticProp
Egenskapsdekoratør: Instanse-egenskap B på instanceProp
Metode/Aksessor-dekoratør: Getter/Setter F på myAccessor
Metode/Aksessor-dekoratør: Metode C på myMethod
Metode/Aksessor-dekoratør: Metode D på myMethod
Klassedekoratør: Klassenivå C på MyDecoratedClass
Klassedekoratør: Klassenivå D på MyDecoratedClass
Konstruktør utført.
Metoden myMethod utført.
*/
Den nøyaktige timingen for konsolloggen kan variere litt basert på når en konstruktør eller metode kalles, men rekkefølgen som dekoratørfunksjonene selv utføres i (og dermed deres bivirkninger eller returnerte verdier anvendes) følger reglene ovenfor.
Praktiske Anvendelser og Designmønstre med Dekoratører
Dekoratører, spesielt i kombinasjon med reflect-metadata
-polyfillen, åpner opp for en ny verden av metadatadrevet programmering. Dette muliggjør kraftige designmønstre som abstraherer bort standardkode (boilerplate) og tverrgående anliggender.
1. Dependency Injection (DI)
En av de mest fremtredende bruksområdene for dekoratører er i Dependency Injection-rammeverk (som Angulars @Injectable()
, @Component()
, osv., eller NestJS' utstrakte bruk av DI). Dekoratører lar deg deklarere avhengigheter direkte på konstruktører eller egenskaper, noe som gjør at rammeverket automatisk kan instansiere og levere de riktige tjenestene.
Eksempel: Forenklet Tjenesteinjeksjon
import "reflect-metadata"; // Essensielt for emitDecoratorMetadata
const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");
function Injectable() {
return function (target: Function) {
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
};
}
function Inject(token: any) {
return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
existingInjections[parameterIndex] = token;
Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
};
}
class Container {
private static instances = new Map<any, any>();
static resolve<T>(target: { new (...args: any[]): T }): T {
if (Container.instances.has(target)) {
return Container.instances.get(target);
}
const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
if (!isInjectable) {
throw new Error(`Klassen ${target.name} er ikke merket som @Injectable.`);
}
// Hent konstruktørparametrenes typer (krever emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Bruk eksplisitt @Inject-token hvis gitt, ellers inferer typen
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`Kan ikke løse parameter ved indeks ${index} for ${target.name}. Det kan være en sirkulær avhengighet eller primitiv type uten eksplisitt @Inject.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Definer tjenester
@Injectable()
class DatabaseService {
connect() {
console.log("Kobler til database...");
return "DB-tilkobling";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Autentiserer ved hjelp av ${this.db.connect()}`);
return "Bruker logget inn";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Eksempel på injeksjon via egenskap med en tilpasset dekoratør eller rammeverkfunksjon
constructor(@Inject(AuthService) authService: AuthService,
@Inject(DatabaseService) dbService: DatabaseService) {
this.authService = authService;
this.dbService = dbService;
}
getUserProfile() {
this.authService.login();
this.dbService.connect();
console.log("UserService: Henter brukerprofil...");
return { id: 1, name: "Global User" };
}
}
// Løs hovedtjenesten
console.log("--- Løser UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("\n--- Løser AuthService (bør være mellomlagret) ---");
const authService = Container.resolve(AuthService);
authService.login();
Dette forseggjorte eksempelet demonstrerer hvordan @Injectable
- og @Inject
-dekoratører, kombinert med reflect-metadata
, lar en tilpasset Container
automatisk løse opp og levere avhengigheter. design:paramtypes
-metadataen som automatisk sendes ut av TypeScript (når emitDecoratorMetadata
er satt til true) er avgjørende her.
2. Aspektorientert Programmering (AOP)
AOP fokuserer på å modularisere tverrgående anliggender (f.eks. logging, sikkerhet, transaksjoner) som går på tvers av flere klasser og moduler. Dekoratører er en utmerket match for å implementere AOP-konsepter i TypeScript.
Eksempel: Logging med Metodedekoratør
Ved å se tilbake på LogCall
-dekoratøren, er den et perfekt eksempel på AOP. Den legger til loggingsatferd til enhver metode uten å modifisere metodens opprinnelige kode. Dette separerer "hva som skal gjøres" (forretningslogikk) fra "hvordan det skal gjøres" (logging, ytelsesovervåking, osv.).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Går inn i metode: ${String(propertyKey)} med argumenter:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Forlater metode: ${String(propertyKey)} med resultat:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Feil i metode ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error("Betalingsbeløpet må være positivt.");
}
console.log(`Behandler betaling på ${amount} ${currency}...`);
return `Betaling på ${amount} ${currency} behandlet vellykket.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Refunderer betaling for transaksjons-ID: ${transactionId}...`);
return `Refusjon initiert for ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
processor.processPayment(-50, "EUR");
} catch (error: any) {
console.error("Fanget feil:", error.message);
}
Denne tilnærmingen holder PaymentProcessor
-klassen fokusert utelukkende på betalingslogikk, mens LogMethod
-dekoratøren håndterer det tverrgående anliggendet med logging.
3. Validering og Transformasjon
Dekoratører er utrolig nyttige for å definere valideringsregler direkte på egenskaper eller for å transformere data under serialisering/deserialisering.
Eksempel: Datavalidering med Egenskapsdekoratører
@Required
-eksemplet tidligere demonstrerte allerede dette. Her er et annet eksempel med en numerisk område-validering.
interface FieldValidationRule {
property: string | symbol;
validator: (value: any) => boolean;
message: string;
}
const fieldValidationRules = new Map<Function, FieldValidationRule[]>();
function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
const rules = fieldValidationRules.get(target.constructor) || [];
rules.push({ property: propertyKey, validator, message });
fieldValidationRules.set(target.constructor, rules);
}
function IsPositive(target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} må være et positivt tall.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} kan maksimalt være ${maxLength} tegn langt.`);
};
}
class Product {
@MaxLength(50)
name: string;
@IsPositive
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
static validate(instance: any): string[] {
const errors: string[] = [];
const rules = fieldValidationRules.get(instance.constructor) || [];
for (const rule of rules) {
if (!rule.validator(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
}
const product1 = new Product("Laptop", 1200);
console.log("Feil for produkt 1:", Product.validate(product1)); // []
const product2 = new Product("Et veldig langt produktnavn som overstiger femti tegns grensen for testformål", 50);
console.log("Feil for produkt 2:", Product.validate(product2)); // ["name kan maksimalt være 50 tegn langt."]
const product3 = new Product("Bok", -10);
console.log("Feil for produkt 3:", Product.validate(product3)); // ["price må være et positivt tall."]
Dette oppsettet lar deg deklarativt definere valideringsregler på dine modellegenskaper, noe som gjør datamodellene dine selvbeskrivende med hensyn til deres begrensninger.
Beste Praksis og Betraktninger
Selv om dekoratører er kraftige, bør de brukes med omhu. Misbruk av dem kan føre til kode som er vanskeligere å feilsøke eller forstå.
Når man Bør Bruke Dekoratører (og Når Man Ikke Bør)
- Bruk dem for:
- Tverrgående anliggender: Logging, mellomlagring, autorisasjon, transaksjonshåndtering.
- Metadatadeklarasjon: Definere skjema for ORM-er, valideringsregler, DI-konfigurasjon.
- Rammeverksintegrasjon: Når man bygger eller bruker rammeverk som benytter metadata.
- Redusere standardkode: Abstrahere repeterende kodemønstre.
- Unngå dem for:
- Enkle funksjonskall: Hvis et vanlig funksjonskall kan oppnå samme resultat tydelig, foretrekk det.
- Forretningslogikk: Dekoratører bør utvide, ikke definere, kjerneforretningslogikk.
- Overkomplisering: Hvis bruken av en dekoratør gjør koden mindre lesbar eller vanskeligere å teste, revurder.
Ytelsesimplikasjoner
Dekoratører utføres ved kompileringstid (eller definisjonstid i JavaScript-kjøretid hvis transpilert). Transformasjonen eller metadatainnsamlingen skjer når klassen/metoden er definert, ikke ved hvert kall. Derfor er ytelsespåvirkningen ved *anvendelse* av dekoratører minimal. Imidlertid kan *logikken inne i* dine dekoratører ha en ytelsespåvirkning, spesielt hvis de utfører kostbare operasjoner ved hvert metodekall (f.eks. komplekse beregninger i en metodedekoratør).
Vedlikeholdbarhet og Lesbarhet
Dekoratører, når de brukes riktig, kan betydelig forbedre lesbarheten ved å flytte standardkode ut av hovedlogikken. Men hvis de utfører komplekse, skjulte transformasjoner, kan feilsøking bli utfordrende. Sørg for at dine dekoratører er godt dokumentert og at deres atferd er forutsigbar.
Eksperimentell Status og Fremtiden for Dekoratører
Det er viktig å gjenta at TypeScript-dekoratører er basert på et Stage 3 TC39-forslag. Dette betyr at spesifikasjonen er stort sett stabil, men kan fortsatt gjennomgå mindre endringer før den blir en del av den offisielle ECMAScript-standarden. Rammeverk som Angular har omfavnet dem, og satser på deres eventuelle standardisering. Dette innebærer en viss grad av risiko, men gitt deres utbredte adopsjon, er betydelige endringer som bryter med eksisterende kode usannsynlig.
TC39-forslaget har utviklet seg. TypeScript sin nåværende implementasjon er basert på en eldre versjon av forslaget. Det er en distinksjon mellom "Legacy Decorators" og "Standard Decorators". Når den offisielle standarden lander, vil TypeScript sannsynligvis oppdatere sin implementasjon. For de fleste utviklere som bruker rammeverk, vil denne overgangen bli håndtert av rammeverket selv. For bibliotekforfattere kan det bli nødvendig å forstå de subtile forskjellene mellom legacy og fremtidige standarddekoratører.
Kompileringsalternativet emitDecoratorMetadata
Dette alternativet, når satt til true
i tsconfig.json
, instruerer TypeScript-kompilatoren til å sende ut visse designtids typemetadata til den kompilerte JavaScript-koden. Denne metadataen inkluderer typen til konstruktørparametere (design:paramtypes
), returtypen til metoder (design:returntype
), og typen til egenskaper (design:type
).
Denne utsendte metadataen er ikke en del av standard JavaScript-kjøretid. Den konsumeres vanligvis av reflect-metadata
-polyfillen, som deretter gjør den tilgjengelig via Reflect.getMetadata()
-funksjonene. Dette er absolutt kritisk for avanserte mønstre som Dependency Injection, der en container trenger å vite hvilke typer avhengigheter en klasse krever uten eksplisitt konfigurasjon.
Avanserte Mønstre med Dekoratører
Dekoratører kan kombineres og utvides for å bygge enda mer sofistikerte mønstre.
1. Dekorere Dekoratører (Høyere Ordens Dekoratører)
Du kan lage dekoratører som modifiserer eller komponerer andre dekoratører. Dette er mindre vanlig, men demonstrerer den funksjonelle naturen til dekoratører.
// En dekoratør som sikrer at en metode logges og også krever admin-roller
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Anvend Authorization først (indre)
Authorization(["admin"])(target, propertyKey, descriptor);
// Anvend deretter LogCall (ytre)
LogCall(target, propertyKey, descriptor);
return descriptor; // Returner den modifiserte beskrivelsen
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Sletter brukerkonto: ${userId}`);
return `Bruker ${userId} slettet.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Forventet utdata (forutsatt admin-rolle):
[AUTH] Tilgang gitt for deleteUserAccount
[LOG] Kaller deleteUserAccount med argumenter: [ 'user007' ]
Sletter brukerkonto: user007
[LOG] Metode deleteUserAccount returnerte: Bruker user007 slettet.
*/
Her er AdminAndLoggedMethod
en fabrikk som returnerer en dekoratør, og inne i den dekoratøren anvender den to andre dekoratører. Dette mønsteret kan innkapsle komplekse dekoratørkomposisjoner.
2. Bruke Dekoratører for Mixins
Selv om TypeScript tilbyr andre måter å implementere mixins på, kan dekoratører brukes til å injisere egenskaper i klasser på en deklarativ måte.
function ApplyMixins(constructors: Function[]) {
return function (derivedConstructor: Function) {
constructors.forEach(baseConstructor => {
Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
Object.defineProperty(
derivedConstructor.prototype,
name,
Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
);
});
});
};
}
class Disposable {
isDisposed: boolean = false;
dispose() {
this.isDisposed = true;
console.log("Objekt avhendet.");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// Disse egenskapene/metodene blir injisert av dekoratøren
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Ressurs ${this.name} opprettet.`);
}
cleanUp() {
this.dispose();
this.log(`Ressurs ${this.name} ryddet opp.`);
}
}
const resource = new MyResource("NetworkConnection");
console.log(`Er avhendet: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Er avhendet: ${resource.isDisposed}`);
Denne @ApplyMixins
-dekoratøren kopierer dynamisk metoder og egenskaper fra basiskonstruktører til den avledede klassens prototype, og "mikser inn" funksjonaliteter på en effektiv måte.
Konklusjon: Styrking av Moderne TypeScript-utvikling
TypeScript-dekoratører er en kraftig og uttrykksfull funksjon som muliggjør et nytt paradigme av metadatadrevet og aspektorientert programmering. De lar utviklere forbedre, modifisere og legge til deklarativ atferd til klasser, metoder, egenskaper, aksessorer og parametere uten å endre deres kjernelogikk. Denne separasjonen av ansvarsområder fører til renere, mer vedlikeholdbar og høyst gjenbrukbar kode.
Fra å forenkle dependency injection og implementere robuste valideringssystemer til å legge til tverrgående anliggender som logging og ytelsesovervåking, gir dekoratører en elegant løsning på mange vanlige utviklingsutfordringer. Selv om deres eksperimentelle status krever bevissthet, signaliserer deres utbredte adopsjon i store rammeverk deres praktiske verdi og fremtidige relevans.
Ved å mestre TypeScript-dekoratører får du et betydelig verktøy i arsenalet ditt, som gjør deg i stand til å bygge mer robuste, skalerbare og intelligente applikasjoner. Omfavn dem ansvarlig, forstå deres mekanismer, og lås opp et nytt nivå av deklarativ kraft i dine TypeScript-prosjekter.