Bahasa Indonesia

Jelajahi kekuatan Decorator TypeScript untuk pemrograman metadata, pemrograman berorientasi aspek, dan menyempurnakan kode dengan pola deklaratif. Panduan komprehensif untuk pengembang global.

Decorator TypeScript: Menguasai Pola Pemrograman Metadata untuk Aplikasi yang Tangguh

Dalam lanskap pengembangan perangkat lunak modern yang luas, menjaga basis kode yang bersih, dapat diskalakan, dan mudah dikelola adalah hal yang terpenting. TypeScript, dengan sistem tipe yang kuat dan fitur-fitur canggihnya, menyediakan alat bagi pengembang untuk mencapai hal ini. Di antara fitur-fitur yang paling menarik dan transformatif adalah Decorator. Meskipun masih merupakan fitur eksperimental pada saat penulisan ini (proposal Tahap 3 untuk ECMAScript), decorator banyak digunakan dalam kerangka kerja seperti Angular dan TypeORM, yang secara fundamental mengubah cara kita mendekati pola desain, pemrograman metadata, dan pemrograman berorientasi aspek (AOP).

Panduan komprehensif ini akan membahas secara mendalam tentang decorator TypeScript, menjelajahi mekanismenya, berbagai jenis, aplikasi praktis, dan praktik terbaik. Baik Anda membangun aplikasi perusahaan berskala besar, layanan mikro, atau antarmuka web sisi klien, memahami decorator akan memberdayakan Anda untuk menulis kode TypeScript yang lebih deklaratif, mudah dipelihara, dan kuat.

Memahami Konsep Inti: Apa Itu Decorator?

Pada intinya, decorator adalah jenis deklarasi khusus yang dapat dilampirkan ke deklarasi kelas, metode, accessor, properti, atau parameter. Decorator adalah fungsi yang mengembalikan nilai baru (atau memodifikasi yang sudah ada) untuk target yang dihiasnya. Tujuan utamanya adalah untuk menambahkan metadata atau mengubah perilaku deklarasi yang dilampirkan padanya, tanpa memodifikasi struktur kode dasarnya secara langsung. Cara eksternal dan deklaratif untuk menambah kode ini sangat kuat.

Anggap decorator sebagai anotasi atau label yang Anda terapkan pada bagian kode Anda. Label-label ini kemudian dapat dibaca atau ditindaklanjuti oleh bagian lain dari aplikasi Anda atau oleh kerangka kerja, sering kali saat runtime, untuk menyediakan fungsionalitas atau konfigurasi tambahan.

Sintaksis Decorator

Decorator diawali dengan simbol @, diikuti oleh nama fungsi decorator. Mereka ditempatkan tepat sebelum deklarasi yang dihiasnya.

@MyDecorator
class MyClass {
  @AnotherDecorator
  myMethod() {
    // ...
  }
}

Mengaktifkan Decorator di TypeScript

Sebelum Anda dapat menggunakan decorator, Anda harus mengaktifkan opsi kompiler experimentalDecorators di file tsconfig.json Anda. Selain itu, untuk kemampuan refleksi metadata tingkat lanjut (yang sering digunakan oleh kerangka kerja), Anda juga memerlukan emitDecoratorMetadata dan polyfill reflect-metadata.

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Anda juga perlu menginstal reflect-metadata:

npm install reflect-metadata --save
# atau
yarn add reflect-metadata

Dan mengimpornya di bagian paling atas dari titik masuk aplikasi Anda (misalnya, main.ts atau app.ts):

import "reflect-metadata";
// Kode aplikasi Anda menyusul

Decorator Factory: Kustomisasi di Ujung Jari Anda

Meskipun decorator dasar adalah sebuah fungsi, sering kali Anda perlu meneruskan argumen ke decorator untuk mengonfigurasi perilakunya. Hal ini dicapai dengan menggunakan decorator factory. Decorator factory adalah fungsi yang mengembalikan fungsi decorator yang sebenarnya. Saat Anda menerapkan decorator factory, Anda memanggilnya dengan argumennya, dan kemudian ia mengembalikan fungsi decorator yang diterapkan oleh TypeScript pada kode Anda.

Contoh Membuat Decorator Factory Sederhana

Mari kita buat factory untuk decorator Logger yang dapat mencatat pesan dengan awalan yang berbeda.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Kelas ${target.name} telah didefinisikan.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Aplikasi sedang dimulai...");
  }
}

const app = new ApplicationBootstrap();
// Keluaran:
// [APP_INIT] Kelas ApplicationBootstrap telah didefinisikan.
// Aplikasi sedang dimulai...

Dalam contoh ini, Logger("APP_INIT") adalah panggilan decorator factory. Ia mengembalikan fungsi decorator sebenarnya yang mengambil target: Function (konstruktor kelas) sebagai argumennya. Ini memungkinkan konfigurasi dinamis dari perilaku decorator.

Jenis-jenis Decorator di TypeScript

TypeScript mendukung lima jenis decorator yang berbeda, masing-masing berlaku untuk jenis deklarasi tertentu. Tanda tangan (signature) dari fungsi decorator bervariasi berdasarkan konteks di mana ia diterapkan.

1. Decorator Kelas

Decorator kelas diterapkan pada deklarasi kelas. Fungsi decorator menerima konstruktor kelas sebagai satu-satunya argumen. Decorator kelas dapat mengamati, memodifikasi, atau bahkan mengganti definisi kelas.

Tanda Tangan (Signature):

function ClassDecorator(target: Function) { ... }

Nilai Kembalian:

Jika decorator kelas mengembalikan nilai, nilai tersebut akan menggantikan deklarasi kelas dengan fungsi konstruktor yang disediakan. Ini adalah fitur yang kuat, sering digunakan untuk mixin atau augmentasi kelas. Jika tidak ada nilai yang dikembalikan, kelas asli yang digunakan.

Kasus Penggunaan:

Contoh Decorator Kelas: Menyuntikkan Layanan

Bayangkan skenario injeksi dependensi sederhana di mana Anda ingin menandai kelas sebagai "injectable" dan secara opsional memberikan nama untuknya di dalam sebuah wadah.

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(`Layanan terdaftar: ${serviceName}`);

    // Secara opsional, Anda bisa mengembalikan kelas baru di sini untuk menambah perilaku
    return class extends constructor {
      createdAt = new Date();
      // Properti atau metode tambahan untuk semua layanan yang disuntikkan
    };
  };
}

@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("--- Layanan Terdaftar ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Pengguna:", userServiceInstance.getUsers());
  // console.log("Layanan Pengguna Dibuat Pada:", userServiceInstance.createdAt); // Jika kelas yang dikembalikan digunakan
}

Contoh ini menunjukkan bagaimana decorator kelas dapat mendaftarkan kelas dan bahkan memodifikasi konstruktornya. Decorator Injectable membuat kelas dapat ditemukan oleh sistem injeksi dependensi teoretis.

2. Decorator Metode

Decorator metode diterapkan pada deklarasi metode. Mereka menerima tiga argumen: objek target (untuk anggota statis, fungsi konstruktor; untuk anggota instance, prototipe kelas), nama metode, dan deskriptor properti dari metode tersebut.

Tanda Tangan (Signature):

function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Nilai Kembalian:

Decorator metode dapat mengembalikan PropertyDescriptor baru. Jika ya, deskriptor ini akan digunakan untuk mendefinisikan metode. Ini memungkinkan Anda untuk memodifikasi atau mengganti implementasi metode asli, membuatnya sangat kuat untuk AOP.

Kasus Penggunaan:

Contoh Decorator Metode: Pemantauan Kinerja

Mari kita buat decorator MeasurePerformance untuk mencatat waktu eksekusi sebuah 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(`Metode "${propertyKey}" dieksekusi dalam ${duration.toFixed(2)} md`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Mensimulasikan operasi yang kompleks dan memakan waktu
    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 untuk ID: ${id}`);
      }, 500);
    });
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));

Decorator MeasurePerformance membungkus metode asli dengan logika waktu, mencetak durasi eksekusi tanpa mengacaukan logika bisnis di dalam metode itu sendiri. Ini adalah contoh klasik dari Pemrograman Berorientasi Aspek (AOP).

3. Decorator Accessor

Decorator accessor diterapkan pada deklarasi accessor (get dan set). Mirip dengan decorator metode, mereka menerima objek target, nama accessor, dan deskriptor propertinya.

Tanda Tangan (Signature):

function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Nilai Kembalian:

Decorator accessor dapat mengembalikan PropertyDescriptor baru, yang akan digunakan untuk mendefinisikan accessor.

Kasus Penggunaan:

Contoh Decorator Accessor: Caching Getter

Mari kita buat decorator yang menyimpan hasil komputasi getter yang mahal dalam cache.

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] Menghitung nilai untuk ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Menggunakan nilai cache untuk ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

  constructor(data: number[]) {
    this.data = data;
  }

  // Mensimulasikan komputasi yang mahal
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Melakukan perhitungan ringkasan yang mahal...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

const generator = new ReportGenerator([10, 20, 30, 40, 50]);

console.log("Akses pertama:", generator.expensiveSummary);
console.log("Akses kedua:", generator.expensiveSummary);
console.log("Akses ketiga:", generator.expensiveSummary);

Decorator ini memastikan bahwa komputasi getter expensiveSummary hanya berjalan sekali, panggilan berikutnya mengembalikan nilai yang di-cache. Pola ini sangat berguna untuk mengoptimalkan kinerja di mana akses properti melibatkan komputasi berat atau panggilan eksternal.

4. Decorator Properti

Decorator properti diterapkan pada deklarasi properti. Mereka menerima dua argumen: objek target (untuk anggota statis, fungsi konstruktor; untuk anggota instance, prototipe kelas), dan nama properti.

Tanda Tangan (Signature):

function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }

Nilai Kembalian:

Decorator properti tidak dapat mengembalikan nilai apa pun. Penggunaan utamanya adalah untuk mendaftarkan metadata tentang properti. Mereka tidak dapat secara langsung mengubah nilai properti atau deskriptornya pada saat dekorasi, karena deskriptor untuk properti belum sepenuhnya didefinisikan saat decorator properti dijalankan.

Kasus Penggunaan:

Contoh Decorator Properti: Validasi Bidang Wajib

Mari kita buat decorator untuk menandai properti sebagai "wajib" dan kemudian memvalidasinya saat runtime.

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)} wajib diisi.`
  });
  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("Kesalahan validasi Pengguna 1:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("Kesalahan validasi Pengguna 2:", validate(user2)); // ["firstName wajib diisi."]

const user3 = new UserProfile("Alice", "");
console.log("Kesalahan validasi Pengguna 3:", validate(user3)); // ["lastName wajib diisi."]

Decorator Required hanya mendaftarkan aturan validasi dengan map pusat validationRules. Fungsi validate terpisah kemudian menggunakan metadata ini untuk memeriksa instance saat runtime. Pola ini memisahkan logika validasi dari definisi data, membuatnya dapat digunakan kembali dan bersih.

5. Decorator Parameter

Decorator parameter diterapkan pada parameter dalam konstruktor kelas atau metode. Mereka menerima tiga argumen: objek target (untuk anggota statis, fungsi konstruktor; untuk anggota instance, prototipe kelas), nama metode (atau undefined untuk parameter konstruktor), dan indeks ordinal parameter dalam daftar parameter fungsi.

Tanda Tangan (Signature):

function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

Nilai Kembalian:

Decorator parameter tidak dapat mengembalikan nilai apa pun. Seperti decorator properti, peran utama mereka adalah menambahkan metadata tentang parameter.

Kasus Penggunaan:

Contoh Decorator Parameter: Menyuntikkan Data Permintaan

Mari kita simulasikan bagaimana kerangka kerja web mungkin menggunakan decorator parameter untuk menyuntikkan data spesifik ke dalam parameter metode, seperti ID pengguna dari permintaan.

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);
  };
}

// Fungsi kerangka kerja hipotetis untuk memanggil metode dengan parameter yang diselesaikan
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(`Mengambil pengguna dengan ID: ${userId}, Token: ${authToken || "T/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Menghapus pengguna dengan ID: ${userId}`);
    return { status: "dihapus", id: userId };
  }
}

const userController = new UserController();

// Mensimulasikan permintaan masuk
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("\n--- Menjalankan getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("\n--- Menjalankan deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

Contoh ini menunjukkan bagaimana decorator parameter dapat mengumpulkan informasi tentang parameter metode yang diperlukan. Sebuah kerangka kerja kemudian dapat menggunakan metadata yang dikumpulkan ini untuk secara otomatis menyelesaikan dan menyuntikkan nilai yang sesuai saat metode dipanggil, yang secara signifikan menyederhanakan logika controller atau layanan.

Komposisi dan Urutan Eksekusi Decorator

Decorator dapat diterapkan dalam berbagai kombinasi, dan memahami urutan eksekusinya sangat penting untuk memprediksi perilaku dan menghindari masalah yang tidak terduga.

Beberapa Decorator pada Satu Target

Ketika beberapa decorator diterapkan pada satu deklarasi (misalnya, kelas, metode, atau properti), mereka dieksekusi dalam urutan tertentu: dari bawah ke atas, atau kanan ke kiri, untuk evaluasinya. Namun, hasilnya diterapkan dalam urutan yang berlawanan.

@DecoratorA
@DecoratorB
class MyClass {
  // ...
}

Di sini, DecoratorB akan dievaluasi terlebih dahulu, kemudian DecoratorA. Jika mereka memodifikasi kelas (misalnya, dengan mengembalikan konstruktor baru), modifikasi dari DecoratorA akan membungkus atau menerapkan di atas modifikasi dari DecoratorB.

Contoh: Merangkai Decorator Metode

Pertimbangkan dua decorator metode: LogCall dan Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Memanggil ${String(propertyKey)} dengan argumen:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Metode ${String(propertyKey)} mengembalikan:`, 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"]; // Mensimulasikan pengambilan peran pengguna saat ini
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Akses ditolak untuk ${String(propertyKey)}. Peran yang dibutuhkan: ${roles.join(", ")}`);
        throw new Error("Akses tidak sah");
      }
      console.log(`[AUTH] Akses diberikan untuk ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Menghapus data sensitif untuk ID: ${id}`);
    return `Data ID ${id} dihapus.`;
  }

  @Authorization(["user"])
  @LogCall // Urutan diubah di sini
  fetchPublicData(query: string) {
    console.log(`Mengambil data publik dengan kueri: ${query}`);
    return `Data publik untuk kueri: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- Memanggil deleteSensitiveData (Pengguna Admin) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- Memanggil fetchPublicData (Pengguna Non-Admin) ---");
  // Mensimulasikan pengguna non-admin mencoba mengakses fetchPublicData yang memerlukan peran 'user'
  const mockUserRoles = ["guest"]; // Ini akan gagal otentikasi
  // Untuk membuatnya dinamis, Anda memerlukan sistem DI atau konteks statis untuk peran pengguna saat ini.
  // Untuk kesederhanaan, kita asumsikan decorator Authorization memiliki akses ke konteks pengguna saat ini.
  // Mari kita sesuaikan decorator Authorization untuk selalu mengasumsikan 'admin' untuk tujuan demo,
  // sehingga panggilan pertama berhasil dan yang kedua gagal untuk menunjukkan jalur yang berbeda.
  
  // Jalankan kembali dengan peran pengguna agar fetchPublicData berhasil.
  // Bayangkan currentUserRoles di Authorization menjadi: ['user']
  // Untuk contoh ini, mari kita tetap sederhana dan menunjukkan efek urutan.
  service.fetchPublicData("istilah pencarian"); // Ini akan menjalankan Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Keluaran yang diharapkan untuk deleteSensitiveData:
[AUTH] Akses diberikan untuk deleteSensitiveData
[LOG] Memanggil deleteSensitiveData dengan argumen: [ 'record123' ]
Menghapus data sensitif untuk ID: record123
[LOG] Metode deleteSensitiveData mengembalikan: Data ID record123 dihapus.
*/

/* Keluaran yang diharapkan untuk fetchPublicData (jika pengguna memiliki peran 'user'):
[LOG] Memanggil fetchPublicData dengan argumen: [ 'istilah pencarian' ]
[AUTH] Akses diberikan untuk fetchPublicData
Mengambil data publik dengan kueri: istilah pencarian
[LOG] Metode fetchPublicData mengembalikan: Data publik untuk kueri: istilah pencarian
*/

Perhatikan urutannya: untuk deleteSensitiveData, Authorization (bawah) berjalan terlebih dahulu, kemudian LogCall (atas) membungkusnya. Logika internal dari Authorization dieksekusi terlebih dahulu. Untuk fetchPublicData, LogCall (bawah) berjalan terlebih dahulu, kemudian Authorization (atas) membungkusnya. Ini berarti aspek LogCall akan berada di luar aspek Authorization. Perbedaan ini sangat penting untuk masalah lintas-sektoral (cross-cutting concerns) seperti logging atau penanganan kesalahan, di mana urutan eksekusi dapat secara signifikan memengaruhi perilaku.

Urutan Eksekusi untuk Target yang Berbeda

Ketika sebuah kelas, anggota-anggotanya, dan parameternya semuanya memiliki decorator, urutan eksekusinya terdefinisi dengan baik:

  1. Decorator Parameter diterapkan terlebih dahulu, untuk setiap parameter, mulai dari parameter terakhir hingga yang pertama.
  2. Kemudian, Decorator Metode, Accessor, atau Properti diterapkan untuk setiap anggota.
  3. Terakhir, Decorator Kelas diterapkan pada kelas itu sendiri.

Dalam setiap kategori, beberapa decorator pada target yang sama diterapkan dari bawah ke atas (atau kanan ke kiri).

Contoh: Urutan Eksekusi Penuh

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Decorator Param: ${message} pada parameter #${descriptorOrIndex} dari ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Decorator Metode/Accessor: ${message} pada ${String(propertyKey)}`);
      } else {
        console.log(`Decorator Properti: ${message} pada ${String(propertyKey)}`);
      }
    } else {
      console.log(`Decorator Kelas: ${message} pada ${target.name}`);
    }
    return descriptorOrIndex; // Kembalikan deskriptor untuk metode/accessor, undefined untuk yang lain
  };
}

@log("Tingkat Kelas D")
@log("Tingkat Kelas C")
class MyDecoratedClass {
  @log("Properti Statis A")
  static staticProp: string = "";

  @log("Properti Instance B")
  instanceProp: number = 0;

  @log("Metode D")
  @log("Metode C")
  myMethod(
    @log("Parameter Z") paramZ: string,
    @log("Parameter Y") paramY: number
  ) {
    console.log("Metode myMethod dieksekusi.");
  }

  @log("Getter/Setter F")
  get myAccessor() {
    return "";
  }

  set myAccessor(value: string) {
    //...
  }

  constructor() {
    console.log("Konstruktor dieksekusi.");
  }
}

new MyDecoratedClass();
// Panggil metode untuk memicu decorator metode
new MyDecoratedClass().myMethod("hello", 123);

/* Urutan Keluaran yang Diprediksi (perkiraan, tergantung pada versi TypeScript dan kompilasi spesifik):
Decorator Param: Parameter Y pada parameter #1 dari myMethod
Decorator Param: Parameter Z pada parameter #0 dari myMethod
Decorator Properti: Properti Statis A pada staticProp
Decorator Properti: Properti Instance B pada instanceProp
Decorator Metode/Accessor: Getter/Setter F pada myAccessor
Decorator Metode/Accessor: Metode C pada myMethod
Decorator Metode/Accessor: Metode D pada myMethod
Decorator Kelas: Tingkat Kelas C pada MyDecoratedClass
Decorator Kelas: Tingkat Kelas D pada MyDecoratedClass
Konstruktor dieksekusi.
Metode myMethod dieksekusi.
*/

Waktu log konsol yang tepat mungkin sedikit bervariasi berdasarkan kapan konstruktor atau metode dipanggil, tetapi urutan di mana fungsi decorator itu sendiri dieksekusi (dan dengan demikian efek samping atau nilai kembaliannya diterapkan) mengikuti aturan di atas.

Aplikasi Praktis dan Pola Desain dengan Decorator

Decorator, terutama dalam hubungannya dengan polyfill reflect-metadata, membuka ranah baru pemrograman yang didorong oleh metadata. Ini memungkinkan pola desain yang kuat yang mengabstraksi boilerplate dan masalah lintas-sektoral.

1. Injeksi Dependensi (DI)

Salah satu penggunaan decorator yang paling menonjol adalah dalam kerangka kerja Injeksi Dependensi (seperti @Injectable(), @Component(), dll. dari Angular, atau penggunaan DI yang luas dari NestJS). Decorator memungkinkan Anda mendeklarasikan dependensi secara langsung pada konstruktor atau properti, memungkinkan kerangka kerja untuk secara otomatis membuat instance dan menyediakan layanan yang benar.

Contoh: Injeksi Layanan yang Disederhanakan

import "reflect-metadata"; // Penting untuk 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(`Kelas ${target.name} tidak ditandai sebagai @Injectable.`);
    }

    // Dapatkan tipe parameter konstruktor (memerlukan emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Gunakan token @Inject eksplisit jika disediakan, jika tidak, infer tipe
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Tidak dapat menyelesaikan parameter di indeks ${index} untuk ${target.name}. Mungkin ada dependensi sirkular atau tipe primitif tanpa @Inject eksplisit.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// Definisikan layanan
@Injectable()
class DatabaseService {
  connect() {
    console.log("Menghubungkan ke database...");
    return "Koneksi DB";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Mengautentikasi menggunakan ${this.db.connect()}`);
    return "Pengguna berhasil login";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Contoh menyuntikkan melalui properti menggunakan decorator kustom atau fitur kerangka kerja

  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: Mengambil profil pengguna...");
    return { id: 1, name: "Pengguna Global" };
  }
}

// Selesaikan layanan utama
console.log("--- Menyelesaikan UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Menyelesaikan AuthService (seharusnya sudah di-cache) ---");
const authService = Container.resolve(AuthService);
authService.login();

Contoh yang rumit ini menunjukkan bagaimana decorator @Injectable dan @Inject, dikombinasikan dengan reflect-metadata, memungkinkan Container kustom untuk secara otomatis menyelesaikan dan menyediakan dependensi. Metadata design:paramtypes yang secara otomatis dipancarkan oleh TypeScript (ketika emitDecoratorMetadata bernilai true) sangat penting di sini.

2. Pemrograman Berorientasi Aspek (AOP)

AOP berfokus pada modularisasi masalah lintas-sektoral (misalnya, logging, keamanan, transaksi) yang melintasi beberapa kelas dan modul. Decorator sangat cocok untuk mengimplementasikan konsep AOP di TypeScript.

Contoh: Logging dengan Decorator Metode

Melihat kembali decorator LogCall, ini adalah contoh sempurna dari AOP. Ia menambahkan perilaku logging ke metode apa pun tanpa memodifikasi kode asli metode tersebut. Ini memisahkan "apa yang harus dilakukan" (logika bisnis) dari "bagaimana melakukannya" (logging, pemantauan kinerja, dll.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Memasuki metode: ${String(propertyKey)} dengan argumen:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Keluar dari metode: ${String(propertyKey)} dengan hasil:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Kesalahan pada metode ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Jumlah pembayaran harus positif.");
    }
    console.log(`Memproses pembayaran sebesar ${amount} ${currency}...`);
    return `Pembayaran sebesar ${amount} ${currency} berhasil diproses.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Mengembalikan dana pembayaran untuk ID transaksi: ${transactionId}...`);
    return `Pengembalian dana dimulai untuk ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.error("Menangkap kesalahan:", error.message);
}

Pendekatan ini menjaga kelas PaymentProcessor tetap fokus murni pada logika pembayaran, sementara decorator LogMethod menangani masalah lintas-sektoral yaitu logging.

3. Validasi dan Transformasi

Decorator sangat berguna untuk mendefinisikan aturan validasi secara langsung pada properti atau untuk mengubah data selama serialisasi/deserialisasi.

Contoh: Validasi Data dengan Decorator Properti

Contoh @Required sebelumnya sudah menunjukkan ini. Berikut adalah contoh lain dengan validasi rentang numerik.

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)} harus berupa angka positif.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} harus paling banyak ${maxLength} karakter.`);
  };
}

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("Kesalahan Produk 1:", Product.validate(product1)); // []

const product2 = new Product("Nama produk yang sangat panjang yang melebihi batas lima puluh karakter untuk tujuan pengujian", 50);
console.log("Kesalahan Produk 2:", Product.validate(product2)); // ["name harus paling banyak 50 karakter."]

const product3 = new Product("Buku", -10);
console.log("Kesalahan Produk 3:", Product.validate(product3)); // ["price harus berupa angka positif."]

Pengaturan ini memungkinkan Anda untuk secara deklaratif mendefinisikan aturan validasi pada properti model Anda, membuat model data Anda mendeskripsikan diri sendiri dalam hal batasan mereka.

Praktik Terbaik dan Pertimbangan

Meskipun decorator sangat kuat, mereka harus digunakan dengan bijaksana. Penyalahgunaan dapat menyebabkan kode yang lebih sulit untuk di-debug atau dipahami.

Kapan Menggunakan Decorator (dan Kapan Tidak)

Implikasi Kinerja

Decorator dieksekusi pada waktu kompilasi (atau waktu definisi dalam runtime JavaScript jika ditranspilasi). Transformasi atau pengumpulan metadata terjadi ketika kelas/metode didefinisikan, bukan pada setiap panggilan. Oleh karena itu, dampak kinerja runtime dari *menerapkan* decorator minimal. Namun, *logika di dalam* decorator Anda dapat memiliki dampak kinerja, terutama jika mereka melakukan operasi yang mahal pada setiap panggilan metode (misalnya, perhitungan kompleks dalam decorator metode).

Keterpeliharaan dan Keterbacaan

Decorator, ketika digunakan dengan benar, dapat secara signifikan meningkatkan keterbacaan dengan memindahkan kode boilerplate keluar dari logika utama. Namun, jika mereka melakukan transformasi yang kompleks dan tersembunyi, debugging bisa menjadi tantangan. Pastikan decorator Anda didokumentasikan dengan baik dan perilakunya dapat diprediksi.

Status Eksperimental dan Masa Depan Decorator

Penting untuk menegaskan kembali bahwa decorator TypeScript didasarkan pada proposal TC39 Tahap 3. Ini berarti spesifikasinya sebagian besar stabil tetapi masih bisa mengalami perubahan kecil sebelum menjadi bagian dari standar ECMAScript resmi. Kerangka kerja seperti Angular telah mengadopsinya, bertaruh pada standardisasi akhirnya. Ini menyiratkan tingkat risiko tertentu, meskipun mengingat adopsi mereka yang luas, perubahan besar yang merusak kemungkinan tidak akan terjadi.

Proposal TC39 telah berevolusi. Implementasi TypeScript saat ini didasarkan pada versi proposal yang lebih lama. Ada perbedaan antara "Legacy Decorators" vs. "Standard Decorators". Ketika standar resmi mendarat, TypeScript kemungkinan akan memperbarui implementasinya. Bagi sebagian besar pengembang yang menggunakan kerangka kerja, transisi ini akan dikelola oleh kerangka kerja itu sendiri. Bagi penulis pustaka, memahami perbedaan halus antara decorator warisan dan standar di masa depan mungkin menjadi perlu.

Opsi Kompiler emitDecoratorMetadata

Opsi ini, ketika diatur ke true di tsconfig.json, menginstruksikan kompiler TypeScript untuk memancarkan metadata tipe waktu desain tertentu ke dalam JavaScript yang dikompilasi. Metadata ini termasuk tipe parameter konstruktor (design:paramtypes), tipe kembalian metode (design:returntype), dan tipe properti (design:type).

Metadata yang dipancarkan ini bukan bagian dari runtime JavaScript standar. Biasanya dikonsumsi oleh polyfill reflect-metadata, yang kemudian membuatnya dapat diakses melalui fungsi Reflect.getMetadata(). Ini sangat penting untuk pola tingkat lanjut seperti Injeksi Dependensi, di mana sebuah wadah perlu mengetahui tipe dependensi yang dibutuhkan kelas tanpa konfigurasi eksplisit.

Pola Lanjutan dengan Decorator

Decorator dapat digabungkan dan diperluas untuk membangun pola yang lebih canggih.

1. Mendekorasi Decorator (Decorator Tingkat Tinggi)

Anda dapat membuat decorator yang memodifikasi atau menyusun decorator lain. Ini kurang umum tetapi menunjukkan sifat fungsional dari decorator.

// Decorator yang memastikan metode dicatat dan juga memerlukan peran admin
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Terapkan Authorization terlebih dahulu (dalam)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Kemudian terapkan LogCall (luar)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Kembalikan deskriptor yang dimodifikasi
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Menghapus akun pengguna: ${userId}`);
    return `Pengguna ${userId} dihapus.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Keluaran yang Diharapkan (dengan asumsi peran admin):
[AUTH] Akses diberikan untuk deleteUserAccount
[LOG] Memanggil deleteUserAccount dengan argumen: [ 'user007' ]
Menghapus akun pengguna: user007
[LOG] Metode deleteUserAccount mengembalikan: Pengguna user007 dihapus.
*/

Di sini, AdminAndLoggedMethod adalah factory yang mengembalikan decorator, dan di dalam decorator itu, ia menerapkan dua decorator lain. Pola ini dapat merangkum komposisi decorator yang kompleks.

2. Menggunakan Decorator untuk Mixin

Meskipun TypeScript menawarkan cara lain untuk mengimplementasikan mixin, decorator dapat digunakan untuk menyuntikkan kemampuan ke dalam kelas secara deklaratif.

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("Objek dibuang.");
  }
}

class Loggable {
  log(message: string) {
    console.log(`[Loggable] ${message}`);
  }
}

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Properti/metode ini disuntikkan oleh decorator
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Sumber daya ${this.name} dibuat.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Sumber daya ${this.name} dibersihkan.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Sudah dibuang: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Sudah dibuang: ${resource.isDisposed}`);

Decorator @ApplyMixins ini secara dinamis menyalin metode dan properti dari konstruktor dasar ke prototipe kelas turunan, secara efektif "mencampurkan" fungsionalitas.

Kesimpulan: Memberdayakan Pengembangan TypeScript Modern

Decorator TypeScript adalah fitur yang kuat dan ekspresif yang memungkinkan paradigma baru pemrograman yang didorong oleh metadata dan berorientasi aspek. Mereka memungkinkan pengembang untuk menyempurnakan, memodifikasi, dan menambahkan perilaku deklaratif ke kelas, metode, properti, accessor, dan parameter tanpa mengubah logika inti mereka. Pemisahan masalah ini mengarah pada kode yang lebih bersih, lebih mudah dipelihara, dan sangat dapat digunakan kembali.

Dari menyederhanakan injeksi dependensi dan mengimplementasikan sistem validasi yang kuat hingga menambahkan masalah lintas-sektoral seperti logging dan pemantauan kinerja, decorator memberikan solusi elegan untuk banyak tantangan pengembangan umum. Meskipun status eksperimentalnya memerlukan kewaspadaan, adopsi mereka yang luas dalam kerangka kerja utama menandakan nilai praktis dan relevansi masa depan mereka.

Dengan menguasai decorator TypeScript, Anda mendapatkan alat yang signifikan dalam persenjataan Anda, memungkinkan Anda untuk membangun aplikasi yang lebih tangguh, dapat diskalakan, dan cerdas. Gunakan mereka secara bertanggung jawab, pahami mekanismenya, dan buka tingkat kekuatan deklaratif baru dalam proyek TypeScript Anda.