⚡ Jawaban Singkat / Key Takeaways: TC39 decorators (Stage 3) adalah standar ECMAScript baru yang menggantikan experimental decorators TypeScript lama. Dengan native decorator, kamu bisa implementasi @validate, @Log, dan @Memoize tanpa polyfill, tanpa reflect-metadata, dan dengan syntax 2-parameter yang lebih bersih. Artikel ini ngasih kamu kode produksi siap pakai untuk tiga cross-cutting concerns paling umum di backend.

Kamu nulis validasi di 47 endpoint. Copy-paste logic yang sama. Trus revisi datang. Kamu refactor. 47 tempat. Capek? Pasti.

Masalahnya klasik: cross-cutting concerns seperti validasi, logging, dan caching tersebar di seluruh codebase. DRY principle hancur. Untungnya, decorator adalah senjata rahasia yang bikin kode kamu tetap bersih. Dan sekarang, dengan TC39 decorators yang udah stage 3 dan didukung penuh TypeScript 5.6+, waktunya kamu tinggalin pola lama dan adopsi cara native.

Di artikel ini, kamu bakal lihat tiga implementasi TC39 decorators yang langsung bisa kamu comot dan pakai di production: @validate untuk validasi input, @Log untuk tracing otomatis, dan @Memoize untuk caching hasil fungsi mahal. Semua pakai syntax baru. Zero dependency.

Kenapa Cross-Cutting Concerns Harus Pakai Decorator?

Bayangin kamu punya class OrderService dengan 15 method. Setiap method butuh validasi parameter. Setiap method butuh logging. Dan tiga method paling mahal butuh caching. Tanpa decorator, kodenya jadi begini:

// Tanpa decorator — nightmare
class OrderService {
  async createOrder(params: CreateOrderDTO) {
    // validasi — copy-paste ke 15 method
    if (!params.userId || !params.items?.length) {
      throw new Error('Invalid params');
    }
    // logging — copy-paste ke 15 method
    console.log(`[${new Date().toISOString()}] createOrder called`);
    // ... business logic akhirnya
  }
}

Pattern ini bikin technical debt numpuk. Setiap revisi aturan validasi, kamu harus hunting ke semua method. Decorator memisahkan concern ini dari business logic utama. Hasilnya: kode yang single-responsibility dan gampang di-test.

1. @validate: Validasi Input Otomatis dengan TC39 Decorator

Validasi adalah cross-cutting concern nomor satu di backend. API layer selalu jadi gerbang masuk data kotor. Dengan TC39 method decorator, kamu bisa bikin @validate yang reusable untuk semua method:

// TC39 Stage 3 — @validate decorator
function validate(schema: ZodSchema) {
  return function (
    _target: Function,
    context: ClassMethodDecoratorContext
  ) {
    const methodName = String(context.name);
    return function (this: any, ...args: any[]) {
      const result = schema.safeParse(args[0]);
      if (!result.success) {
        throw new ValidationError(
          `[${methodName}] ${result.error.message}`
        );
      }
      return _target.apply(this, [result.data, ...args.slice(1)]);
    };
  };
}

// Pemakaian — bersih banget
class UserController {
  @validate(createUserSchema)
  async createUser(dto: CreateUserDTO) {
    // dto udah tervalidasi, tinggal eksekusi
    return this.userRepo.save(dto);
  }
}

Perhatikan bedanya dengan experimental decorator lama:

  • 2 parameter, bukan 3. _target (function asli) dan context (metadata dekorasi).
  • Return replacement function, bukan mutasi descriptor.value. Idiomatic JavaScript.
  • context.name ngasih nama method secara otomatis, berguna buat error message.

Satu baris @validate(createUserSchema) menggantikan 5-7 baris kode validasi manual. Dan yang paling penting: konsisten di seluruh codebase.

2. @Log: Tracing Otomatis Tanpa Nodai Business Logic

Logging di production sering kali jadi afterthought. Developer nambahin console.log saat debug, terus lupa hapus. Atau logging pattern-nya beda-beda tiap file. Dengan @Log, kamu standarisasi semua logging method:

// TC39 @Log decorator — production-grade
function Log(options?: { level?: 'info' | 'warn' | 'error' }) {
  const level = options?.level ?? 'info';

  return function (
    target: Function,
    context: ClassMethodDecoratorContext
  ) {
    return function (this: any, ...args: any[]) {
      const start = performance.now();
      const result = target.apply(this, args);
      
      // Handle promise dan sync function
      const done = (res: any) => {
        const ms = (performance.now() - start).toFixed(2);
        console[level](
          `[${context.name}] ${ms}ms args=${JSON.stringify(args).slice(0, 200)}`
        );
        return res;
      };

      if (result instanceof Promise) {
        return result.then(done).catch((err) => {
          console.error(`[${context.name}] FAILED`, err.message);
          throw err;
        });
      }
      return done(result);
    };
  };
}

// Pemakaian — nggak ganggu logic sama sekali
class PaymentService {
  @Log({ level: 'info' })
  async processRefund(orderId: string, amount: number) {
    // Business logic — no logging noise
    return stripe.refunds.create({ payment_intent: orderId, amount });
  }
}

Yang menarik: decorator ini otomatis mengukur execution time pakai performance.now(). Kamu dapet observability gratis. Dan karena decorator menangani Promise, tracing tetap jalan di async context. Pola ini mirip dengan yang dipakai tim infrastructure di Monzo dan Stripe untuk internal tooling mereka.

3. @Memoize: Caching Otomatis dengan Map

Fungsi mahal seperti kalkulasi pajak, query agregasi, atau komputasi matriks sering dipanggil berulang dengan parameter yang sama. @Memoize menyimpan hasil di memory sehingga pemanggilan berikutnya instant:

// TC39 @Memoize — pakai Map untuk auto-cleanup
function Memoize(options?: { ttlMs?: number }) {
  return function (
    target: Function,
    context: ClassMethodDecoratorContext
  ) {
    const cache = new Map<string, { value: any; timestamp: number }>();

    return function (this: any, ...args: any[]) {
      const key = JSON.stringify(args);
      const cached = cache.get(key);

      // Cache hit + TTL check
      if (cached) {
        if (!options?.ttlMs || Date.now() - cached.timestamp < options.ttlMs) {
          return cached.value;
        }
      }

      const result = target.apply(this, args);
      
      // Simpan hasil (handle Promise)
      if (result instanceof Promise) {
        return result.then((val) => {
          cache.set(key, { value: val, timestamp: Date.now() });
          return val;
        });
      }

      cache.set(key, { value: result, timestamp: Date.now() });
      return result;
    };
  };
}

// Pemakaian — fungsi berat jadi ringan
class TaxCalculator {
  @Memoize({ ttlMs: 300_000 }) // cache 5 menit
  calculateAnnualTax(salary: number, country: string, year: number) {
    // Simulasi kalkulasi berat (DB query + formula kompleks)
    return this.taxRepo.compute(salary, country, year);
  }
}

Yang bikin @Memoize ini powerful:

  • TTL (Time to Live) opsional. Cache expired otomatis setelah waktu tertentu.
  • Async-safe. Promise ditangani dengan benar, nggak double-cache undefined.
  • Map sebagai cache store. Key berasal dari args yang di-stringify. Untuk class method yang butuh auto-GC saat instance dihapus, kamu bisa ganti ke WeakMap dengan this sebagai key.

Pola ini bisa kamu terapkan di method yang memanggil third-party API berbayar. Misalnya, API geocoding Google yang charge per request. Satu baris @Memoize bisa hemat ratusan dolar per bulan.

Gabungin Tiga Decorator Sekaligus: Stacking

Kekuatan asli decorator muncul saat kamu menumpuknya. Satu method bisa pakai validasi, logging, dan caching sekaligus. TC39 decorator dieksekusi dari bawah ke atas (inner first), mirip model onion:

class SubscriptionService {
  @Memoize({ ttlMs: 600_000 })
  @Log({ level: 'info' })
  @validate(renewalSchema)
  async renewSubscription(dto: RenewDTO) {
    // validate → Log → Memoize → business logic
    return this.billingGateway.charge(dto);
  }
}

Urutan eksekusi: validate dulu (buat ngecek input valid), lalu Log (catat durasi dan args), terakhir Memoize (cache hasilnya). Kalau validasi gagal, logging dan caching nggak disentuh sama sekali. Ini adalah fail-fast pattern yang alami tanpa if-else bertumpuk.

Migrasi dari Pola Lama ke TC39

Kalau kamu masih pakai experimental decorators dengan reflect-metadata, transisinya nggak sesulit yang kamu kira. Dua perbedaan utama yang harus kamu perhatikan:

AspekExperimental (Lama)TC39 (Baru)
Parameter3 param (target, key, descriptor)2 param (value, context)
ReturnMutasi descriptor.valueReturn replacement function
MetadataReflect.defineMetadata()Symbol.metadata
DependencyButuh reflect-metadataZero dependencies

Tiga decorator di atas (@validate, @Log, @Memoize) semuanya sudah pakai syntax TC39 murni. Nggak ada dependency reflect-metadata. Kalau project kamu masih mixed, baca juga panduan migrasi lengkap TC39 decorator yang udah aku tulis sebelumnya.

Perangkap yang Sering Bikin Developer Tersandung

Berdasarkan pengalaman migrasi codebase dari experimental ke TC39, ini tiga jebakan paling umum:

  1. Decorator tidak bisa dipakai di constructor. TC39 tidak support @decorator di constructor parameter. Kalau kamu butuh injection di constructor, gunakan Symbol.metadata manual atau tetap pakai DI container framework.
  2. Initializer list dieksekusi sebelum decorator. Field initialization jalan duluan, baru decorator. Ini berbeda dengan experimental decorator. Efeknya: kamu nggak bisa akses field yang didekorasi di dalam decorator kecuali lewat context.addInitializer().
  3. this binding di replacement function. Pastikan kamu pakai function (bukan arrow) untuk replacement, atau this bakal undefined. Arrow function mewarisi this dari parent scope di mana decorator didefinisikan, bukan dari instance class.

FAQ: TC39 Decorators untuk Backend Developer

Apa bedanya TC39 decorator sama HOC (Higher-Order Components) di React?

HOC adalah pattern JavaScript untuk membungkus komponen, sedangkan TC39 decorator adalah syntax standar ECMAScript untuk memodifikasi class/method/field. Decorator berjalan di level bahasa (bukan runtime pattern), dieksekusi saat class definition, dan punya akses ke context metadata. HOC lebih cocok untuk React component composition; decorator lebih cocok untuk backend cross-cutting concerns.

Apakah @Memoize aman untuk concurrent request di Node.js?

Implementasi @Memoize di artikel ini menyimpan cache di Map yang bersifat in-memory dan per-instance. Untuk single-thread Node.js, ini aman karena event loop memproses satu callback dalam satu waktu. Tapi kalau kamu jalankan multiple instances (PM2 cluster atau Kubernetes pod), tiap instance punya cache sendiri. Untuk cache terdistribusi, kombinasikan dengan Redis dan gunakan pattern stale-while-revalidate.

TC39 decorators udah bisa dipakai di production tanpa TypeScript?

Belum sepenuhnya. V8 (Chrome/Node.js) sudah implementasi TC39 decorator secara native sejak 2024, tapi hanya untuk method dan class decorator (field decorator masih di belakang flag). Untuk production, tetap gunakan TypeScript 5.6+ sebagai transpiler. Target ES2023 ke atas agar output kode langsung kompatibel dengan native engine tanpa polyfill tambahan.

Gimana cara test method yang pakai decorator @validate atau @Log?

Testing method berdekorator sama seperti method biasa karena decorator hanya wrapper. Untuk @validate, test dengan input valid dan invalid untuk memastikan error handling. Untuk @Log, spy console.info/console.error dengan Jest/Vitest. Jangan lupa test stack decorator (multiple decorators) untuk memastikan urutan eksekusi sesuai yang kamu ekspektasikan.

Kesimpulan

TC39 decorators bukan sekadar ganti syntax. Ini adalah upgrade fundamental ke cara kamu menulis cross-cutting concerns. Dengan @validate, @Log, dan @Memoize yang native, kamu ngurangin boilerplate, ningkatin konsistensi kode, dan dapet observability gratis. Semua tanpa satu pun dependency eksternal.

Mulai dari satu file, satu decorator. Pilih method yang paling banyak copy-paste validasi. Ganti dengan @validate. Lihat perbedaannya. Lalu lanjut ke @Log di service layer. Terakhir, inject @Memoize ke fungsi yang paling sering dipanggil dengan parameter repetitif.

Baca juga spesifikasi resmi di TC39 Decorator Proposal, cek TypeScript 5.6 Release Notes, dan pelajari bagaimana V8 mengimplementasi decorator di V8 Decorators Guide. Untuk eksplorasi framework TypeScript yang udah TC39-ready, baca juga 7 Framework TypeScript Terbaik 2026.

Udah coba implementasi @Memoize di project kamu? Atau nemu edge case yang belum dibahas? Share di kolom komentar. Kode kamu mungkin jadi solusi buat developer lain yang lagi transisi ke TC39 decorator.

About the Author

Dzul Qurnain

Suka nonton Anime, ngoding dan bagi-bagi tips kalau tahu.. Oh iya, suka baca ( tapi yang menarik menurutku aja)... Praktisi WordPress, web development, SEO, dan server administration yang membagikan tutorial teknis dan catatan implementasi nyata.

View All Articles