Langsung ke konten
KamusNgoding
Mahir Typescript 5 menit baca

Panduan Lengkap Decorators di TypeScript

#typescript #decorators #metaprogramming #advanced

Panduan Lengkap Decorators di TypeScript

Pendahuluan

Jika kamu sudah cukup dalam menyelami TypeScript, pasti pernah menemukan si simbol @ yang menempel di atas sebuah class atau method. Itulah **decorat decorator — salah satu fitur paling powerful sekaligus paling sering di disalahpahami di TypeScript.

Decorator bukan sekadar sintaks manis. Di baliknya, ada mekanisme metaprogr metaprogramming yang memungkinkan kamu memodifikasi perilaku class, method, method, property, hingga parameter tanpa mengubah kode aslinya. Framework Framework seperti NestJS, Angular, dan TypeORM sangat bergantung pada fitur fitur ini.

Dalam artikel ini, kita akan membedah cara kerja decorator dari dalam — mul mulai dari klasifikasinya, cara kerjanya di level JavaScript runtime, hingg hingga bagaimana membangun decorator kustom yang benar-benar berguna. Jika  kamu ingin membangun layanan backend seperti Tokopedia atau Gojek dengan ar arsitektur yang bersih dan modular, memahami decorator adalah langkah yang  wajib.

Catatan: Pastikan tsconfig.json kamu mengaktifkan "experimentalDec ”experimentalDecorators”: truedan”emitDecoratorMetadata”: true` untuk  menggunakan fitur ini. Jika belum familiar dengan konfigurasi tsconfig, kam kamu bisa baca [Tutorial Konfigurasi tsconfig.json untuk Pemula](/docs/sw/tK PemulaPemula](/docs/sw/tpescript/tutorial-konfigurasi-tsconfigjson-untuk-pemula) terlebih dahulu.


Konsep Dasar

Apa itu Decorator?

Decorator adalah sebuah fungsi biasa yang dipanggil secara otomatis oleh Ty TypeScript pada saat deklarasi, bukan saat runtime eksekusi. Ia menerima in informasi tentang target yang didekorasi dan bisa memodifikasi atau menggan mengganti target tersebut.

Ada lima jenis decorator di TypeScript:

JenisTargetArgumen yang diterima
Class DecoratorConstructor classconstructor
Method DecoratorMethod di classtarget, propertyKey, `descri
descriptor
Property DecoratorProperty di classtarget, propertyKey
Parameter DecoratorParameter methodtarget, propertyKey, `pa
parameterIndex
Accessor DecoratorGetter/settertarget, propertyKey, `descri
descriptor

Urutan Eksekusi Decorator

Ketika beberapa decorator ditumpuk, urutannya adalah:

  1. Decorator dievaluasi dari atas ke bawah
  2. Decorator dieksekusi dari bawah ke atas (bottom-up)
function First(): MethodDecorator {
  console.log("First: dievaluasi"); // Komentar: decorator factory dievalua
dievaluasi dari atas ke bawah

  return function (
    target: object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor
  ): void {
    console.log("First: dieksekusi"); // Komentar: decorator dieksekusi dar
dari bawah ke atas
  };
}

function Second(): MethodDecorator {
  console.log("Second: dievaluasi"); // Komentar: decorator factory kedua d
dievaluasi setelah First

  return function (
    target: object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor
  ): void {
    console.log("Second: dieksekusi"); // Komentar: decorator terdekat deng
dengan method dieksekusi lebih dulu
  };
}

class Contoh {
  @First()
  @Second()
  method(): void {
    console.log("method dipanggil"); // Komentar: ini hanya berjalan saat m
method dipanggil
  }
}

const contoh = new Contoh();
contoh.method();

/*
# Output yang diharapkan:
# > First: dievaluasi
# > Second: dievaluasi
# > Second: dieksekusi
# > First: dieksekusi
# > method dipanggil
*/

Decorator Factory

Decorator factory adalah fungsi yang mengembalikan decorator. Ini bergu berguna ketika kamu ingin memberi argumen ke decorator.

function Log<T extends (...args: any[]) => any>(prefix: string) {
  // Dekorator method dengan tipe generik agar parameter dan return tetap a
aman
  return function (
    _target: object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<T>
  ): TypedPropertyDescriptor<T> {
    const original = descriptor.value!; // Simpan method asli sebelum dibun
dibungkus

    descriptor.value = function (
      this: ThisParameterType<T>,
      ...args: Parameters<T>
    ): ReturnType<T> {
      console.log(`[${prefix}] Memanggil ${String(key)} dengan args:`, args
args);

      const result = original.apply(this, args) as ReturnType<T>; // Jalank
Jalankan method asli dengan konteks this yang sama

      console.log(`[${prefix}] ${String(key)} mengembalikan:`, result);
      return result;
    } as T;

    return descriptor;
  };
}

class Kalkulator {
  @Log("DEBUG")
  tambah(a: number, b: number): number {
    return a + b;
  }
}

const kalkulator = new Kalkulator();
kalkulator.tambah(4, 6);

/*
# Output yang diharapkan:
# > [DEBUG] Memanggil tambah dengan args: [ 4, 6 ]
# > [DEBUG] tambah mengembalikan: 10
*/

Contoh Kode

1. Class Decorator: Menambah Metadata

Class decorator sering digunakan untuk mendaftarkan class ke sebuah registr registry atau menambahkan property statis.

type Constructor<T = object> = new (...args: any[]) => T;

function Singleton<TBase extends Constructor>(Base: TBase): TBase {
  let instance: InstanceType<TBase> | null = null; // Menyimpan satu instan
instance untuk class yang didekorasi

  return class extends Base {
    constructor(...args: any[]) {
      if (instance) return instance as this; // Jika sudah ada, kembalikan 
instance yang sama

      super(...args); // Jalankan constructor asli hanya saat instance pert
pertama dibuat
      instance = this as InstanceType<TBase>; // Simpan instance pertama se
sebagai singleton
    }
  } as TBase;
}

@Singleton
class DatabaseConnection {
  private static nextId = 1;
  private readonly id: number;

  constructor() {
    this.id = DatabaseConnection.nextId++;
    console.log("Koneksi baru dibuat dengan ID:", this.id);
  }

  getId(): number {
    return this.id;
  }
}

const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();

console.log(db1 === db2);
console.log(db1.getId() === db2.getId());

/*
# Output yang diharapkan:
# > Koneksi baru dibuat dengan ID: 1
# > true
# > true
*/

Ini persis pola yang digunakan framework database: satu koneksi untuk selur seluruh aplikasi.


2. Method Decorator: Logging Otomatis

Method decorator sangat berguna untuk cross-cutting concerns seperti loggin logging, validasi, dan caching — tanpa mencemari logika bisnis utama.

type CacheEntry<T> = {
  value: T;
  expiresAt: number;
};

function Cacheable<Args extends unknown[], Result>(ttlMs: number) {
  return function <This>(
    _target: object,
    methodName: string | symbol,
    descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Prom
Promise<Result>>
  ) {
    const original = descriptor.value;

    if (!original) {
      throw new Error("@Cacheable hanya bisa dipakai pada method async.");
    }

    const cache = new Map<string, CacheEntry<Result>>();

    descriptor.value = async function (this: This, ...args: Args): Promise<
Promise<Result> {
      // # Cache key dibuat dari argumen; cocok untuk argumen yang aman di-
di-JSON.stringify.
      const cacheKey = JSON.stringify(args);
      const cached = cache.get(cacheKey);
      const now = Date.now();

      // # Jika data masih berlaku, langsung kembalikan tanpa memanggil met
method asli.
      if (cached && cached.expiresAt > now) {
        console.log(`[Cache HIT] ${String(methodName)}`);
        return cached.value;
      }

      console.log(`[Cache MISS] ${String(methodName)} - memanggil fungsi as
asli`);

      // # Method asli tetap dipanggil dengan konteks `this` yang benar.
      const value = await original.apply(this, args);

      // # TTL dihitung setelah hasil tersedia agar masa cache lebih akurat
akurat.
      cache.set(cacheKey, {
        value,
        expiresAt: Date.now() + ttlMs,
      });

      return value;
    };

    return descriptor;
  };
}

type Product = {
  id: string;
  name: string;
};

class ProductService {
  @Cacheable<[string], Product>(5_000) // # Cache berlaku selama 5 detik.
  async getProductById(id: string): Promise<Product> {
    // # Simulasi operasi lambat, misalnya query database atau request API.
API.
    await new Promise((resolve) => setTimeout(resolve, 100));
    return { id, name: `Produk ${id}` };
  }
}

async function main() {
  const service = new ProductService();

  await service.getProductById("SKU-001"); // # Cache MISS
  await service.getProductById("SKU-001"); // # Cache HIT
}

main();

/*
# Output yang diharapkan:
# > [Cache MISS] getProductById - memanggil fungsi asli
# > [Cache HIT] getProductById
*/

Bayangkan membangun layanan e-commerce seperti Shopee: decorator @Cacheabl @Cacheable` ini bisa dipakai di ratusan service method tanpa menulis logik logika caching berulang kali.


3. Property Decorator: Validasi Nilai

Property decorator tidak bisa langsung mengubah nilai property, tapi bisa m mendefinisikan ulang property menggunakan Object.defineProperty.

type NumericValidator = (value: number, key: PropertyKey) => void;

type PropertyState = {
  values: WeakMap<object, number>;
  validators: NumericValidator[];
};

const registry = new WeakMap<object, Map<PropertyKey, PropertyState>>();

function getOrCreateState(target: object, key: PropertyKey): PropertyState 
{
  let properties = registry.get(target);

  // Menyimpan metadata validator per prototype agar tidak bercampur antar 
class.
  if (!properties) {
    properties = new Map();
    registry.set(target, properties);
  }

  let state = properties.get(key);

  // WeakMap membuat nilai properti aman per instance, bukan shared di prot
prototype.
  if (!state) {
    state = { values: new WeakMap<object, number>(), validators: [] };
    properties.set(key, state);

    Object.defineProperty(target, key, {
      get(this: object) {
        return state.values.get(this);
      },
      set(this: object, newValue: number) {
        if (typeof newValue !== "number" || Number.isNaN(newValue)) {
          throw new TypeError(`Property "${String(key)}" harus berupa numbe
number`);
        }

        // Semua validator dari decorator yang ditumpuk dijalankan di satu 
setter.
        for (const validate of state.validators) {
          validate(newValue, key);
        }

        state.values.set(this, newValue);
      },
      enumerable: true,
      configurable: true,
    });
  }

  return state;
}

function Min(minValue: number) {
  return function (target: object, key: PropertyKey) {
    getOrCreateState(target, key).validators.push((value, propertyKey) => {
{
      if (value < minValue) {
        throw new RangeError(
          `Property "${String(propertyKey)}" harus minimal ${minValue}, tap
tapi menerima ${value}`
        );
      }
    });
  };
}

function Max(maxValue: number) {
  return function (target: object, key: PropertyKey) {
    getOrCreateState(target, key).validators.push((value, propertyKey) => {
{
      if (value > maxValue) {
        throw new RangeError(
          `Property "${String(propertyKey)}" tidak boleh lebih dari ${maxVa
${maxValue}, tapi menerima ${value}`
        );
      }
    });
  };
}

class Product {
  constructor(
    public name: string,

    @Min(0)
    @Max(999_999_999)
    public price: number,

    @Min(0)
    public stock: number
  ) {}
}

try {
  const produk = new Product("Laptop", 15_000_000, 10);
  console.log(`Produk valid: ${produk.name} - Rp${produk.price.toLocaleStri
Rp${produk.price.toLocaleString("id-ID")} - stok ${produk.stock}`);

  new Product("Diskon Gila", -5_000, 0);
} catch (error) {
  console.log(`Error: ${(error as Error).message}`);
}

/*
# Output yang diharapkan:
# > Produk valid: Laptop - Rp15.000.000 - stok 10
# > Error: Property "price" harus minimal 0, tapi menerima -5000
*/

4. Akses Kontrol dengan Decorator

Decorator juga bisa digunakan untuk mengimplementasikan kontrol akses pada  method atau property.

function AccessControl(role: string) {
  return function (target: object, key: string | symbol, descriptor: Proper
PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      if (this.role !== role) {
        throw new Error(`Access denied. Required role: ${role}`);
      }
      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

class User {
  constructor(public name: string, public role: string) {}

  @AccessControl("admin")
  deleteUser(userId: number) {
    console.log(`User with ID ${userId} deleted`);
  }

  updateUser(userId: number) {
    console.log(`User with ID ${userId} updated`);
  }
}

const admin = new User("John Doe", "admin");
admin.deleteUser(1); // Output: User with ID 1 deleted

const user = new User("Jane Doe", "user");
user.updateUser(2); // Output: User with ID 2 updated
user.deleteUser(3); // Error: Access denied. Required role: admin

5. Logging dengan Decorator

Decorator bisa juga digunakan untuk logging.

function LogMethod(target: object, key: string | symbol, descriptor: Proper
PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Executing ${key} with arguments`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${key} returned`, result);
    return result;
  };

  return descriptor;
}

class Calculator {
  @LogMethod
  add(a: number, b: number) {
    return a + b;
  }

  @LogMethod
  multiply(a: number, b: number) {
    return a * b;
  }
}

const calc = new Calculator();
calc.add(2, 3); // Output: Executing add with arguments [ 2, 3 ]
               //         add returned 5

calc.multiply(4, 5); // Output: Executing multiply with arguments [ 4, 5 ]
                    //          multiply returned 20

Pertanyaan yang Sering Diajukan

Apa perbedaan decorator di TypeScript dengan decorator di Python?

Meskipun keduanya menggunakan simbol @, cara kerjanya berbeda. Decorator  Python adalah fungsi higher-order yang dieksekusi saat definisi fungsi dan  mengganti fungsi asli. Decorator TypeScript bekerja pada level class dan  memanfaatkan sistem tipe statis serta metadata reflection. TypeScript decor decorator juga memiliki lima varian (class, method, property, accessor, par parameter) sementara Python lebih fleksibel namun tidak ada standarisasi se serupa.

Apakah decorator sudah menjadi standar resmi JavaScript?

Sampai saat ini, decorator di TypeScript menggunakan proposal TC39 Stage 3  (versi lama disebut “legacy decorators”). TypeScript versi terbaru mulai me mendukung decorator Stage 3 dengan opsi "experimentalDecorators": false.  Keduanya berbeda secara sintaks dan perilaku, jadi perhatikan versi TypeScr TypeScript yang kamu gunakan dan sesuaikan konfigurasi tsconfig.json.

Bagaimana cara men-debug decorator yang tidak bekerja?

Pertama, pastikan experimentalDecorators: true aktif di tsconfig.json.  Kedua, tambahkan console.log di dalam decorator factory untuk memverifika memverifikasi bahwa decorator dipanggil. Ketiga, perhatikan bahwa property  decorator tidak menerima PropertyDescriptor — jika kamu mencoba mengakses mengaksesnya, hasilnya akan undefined. Gunakan Object.defineProperty un untuk memodifikasi property.

Mengapa decorator tidak bisa digunakan pada fungsi biasa (bukan method 

class)?

Ini adalah batasan desain TypeScript. Decorator hanya bekerja dalam konteks konteks class karena mekanismenya bergantung pada prototype objek dan sis sistem metadata yang terikat pada class. Untuk fungsi biasa, gunakan Higher Higher-Order Function (HOF) sebagai alternatif — konsepnya serupa tapi tanp tanpa sintaks @.

Apakah penggunaan decorator mempengaruhi performa aplikasi?

Decorator dieksekusi sekali saat deklarasi class (load time), bukan set setiap kali method dipanggil. Overhead performa biasanya sangat kecil dan d dapat diabaikan. Namun, jika decorator method kamu melakukan operasi berat  (misalnya membuka koneksi baru), itu bisa berdampak. Decorator yang menggun menggunakan closure seperti caching justru bisa meningkatkan performa r runtime secara signifikan.


Kesimpulan

Decorator adalah alat metaprogramming yang mengangkat TypeScript ke level y yang jauh lebih ekspresif. Dengan memahami cara kerja class decorator, meth method decorator, property decorator, dan integrasi reflect-metadata, kam kamu bisa membangun framework mini sendiri, sistem validasi yang elegan, at atau arsitektur yang bersih tanpa mengulang kode yang sama.

Kunci utamanya: decorator adalah fungsi biasa yang dieksekusi saat dekl deklarasi. Begitu kamu memahami ini, seluruh pola lainnya akan terasa logis logis dan mudah dikembangkan.

Selamat belajar dan terus eksplorasi! Jika kamu tertarik mendalami TypeScri TypeScript lebih jauh, jangan ragu untuk menjelajahi artikel-artikel lain d di KamusNgoding — masih banyak topik seru yang menunggumu.

Artikel Terkait