⚡ 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) dancontext(metadata dekorasi). - Return replacement function, bukan mutasi
descriptor.value. Idiomatic JavaScript. context.namengasih 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
WeakMapdenganthissebagai 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:
| Aspek | Experimental (Lama) | TC39 (Baru) |
|---|---|---|
| Parameter | 3 param (target, key, descriptor) | 2 param (value, context) |
| Return | Mutasi descriptor.value | Return replacement function |
| Metadata | Reflect.defineMetadata() | Symbol.metadata |
| Dependency | Butuh reflect-metadata | Zero 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:
- Decorator tidak bisa dipakai di constructor. TC39 tidak support
@decoratordi constructor parameter. Kalau kamu butuh injection di constructor, gunakanSymbol.metadatamanual atau tetap pakai DI container framework. - 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(). thisbinding di replacement function. Pastikan kamu pakaifunction(bukan arrow) untuk replacement, atauthisbakalundefined. Arrow function mewarisithisdari parent scope di mana decorator didefinisikan, bukan dari instance class.
FAQ: TC39 Decorators untuk Backend Developer
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.
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.
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.
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.



