Decorator adalah fungsi yang kamu tempelkan ke class atau method dengan tanda @, untuk menambah perilaku tanpa mengubah isi method-nya. Mirip bungkus kado: barang di dalamnya tetap sama, tapi bungkusnya menambah sesuatu, misalnya kartu ucapan yang ikut terbaca tiap kali kadonya dibuka. Decorator @logged membungkus method total: hitungannya tetap harga kali jumlah, tapi sekarang tiap panggilan dicatat di console.
🧪 Tebakan cepat:
kasir.total(...)dipanggil 3 kali. Berapa kali fungsiloggedsendiri dijalankan: 1, 3, atau 4 kali? Tebak dulu, jawabannya ada di bagian "Kapan decorator jalan".
Method decorator: terima method, kembalikan penggantinya
Sejak TypeScript 5.0, decorator mengikuti standar JavaScript (proposal TC39 tahap 3) dan langsung jalan tanpa pengaturan apa pun. Method decorator adalah fungsi biasa dengan dua parameter:
function logged(method: (...args: number[]) => number, context: ClassMethodDecoratorContext) {
const nama = String(context.name)
return function (this: unknown, ...args: number[]): number {
console.log(`-> ${nama}(${args.join(", ")})`)
const hasil = method.apply(this, args)
console.log(`<- ${nama} = ${hasil}`)
return hasil
}
}
class Kasir {
@logged
total(harga: number, jumlah: number): number {
return harga * jumlah
}
}
const kasir = new Kasir()
kasir.total(15000, 2)
-> total(15000, 2)
<- total = 30000
@loggeddi atastotalartinya "serahkan methodtotalke fungsilogged". Tandanya selalu@lalu nama fungsinya.- Parameter pertama,
method, adalah fungsitotalyang asli. - Parameter kedua,
context, adalah object keterangan:{ kind: "method", name: "total", static: false, private: false, ... }. Dari situloggedtahu namanyatotal. - Yang di-return menggantikan
total. Di sini penggantinya mencatat argumen, memanggilmethodyang asli, mencatat hasilnya, lalu mengembalikannya. Kalau decorator gak me-return apa pun, method-nya tetap yang asli. - Penggantinya ditulis dengan
function, bukan arrow function, supayathistetap objekkasir.
Kapan decorator jalan
Fungsi logged jalan sekali, waktu class Kasir dibuat, sebelum ada yang memanggil total. Yang jalan di tiap panggilan adalah fungsi pengganti yang dia kembalikan. Jadi kalau total dipanggil 3 kali, logged tetap cuma jalan 1 kali, dan penggantinya 3 kali. Itu jawaban tebakannya.
Kalau satu method punya dua decorator, keduanya ditulis bertumpuk:
class Kasir {
@pertama
@kedua
total(harga: number, jumlah: number): number { ... }
}
Yang paling dekat dengan method dipasang duluan: kedua membungkus total, lalu pertama membungkus hasilnya.
Class decorator
Decorator juga bisa ditempel ke class. Dia menerima class-nya sendiri dan context dengan kind: "class":
const daftar: string[] = []
function terdaftar(value: Function, context: ClassDecoratorContext) {
daftar.push(String(context.name))
}
@terdaftar
class Kasir { ... }
console.log(daftar) // ["Kasir"]
Begitu class Kasir dibuat, namanya masuk ke daftar. Pola ini yang dipakai framework untuk mendaftarkan class, misalnya @Component di Angular atau @Controller di NestJS.
Dua versi: standar dan experimentalDecorators
Sebelum TypeScript 5.0, decorator cuma ada versi lama yang dinyalakan dengan "experimentalDecorators": true di tsconfig. Versi lama itu berbeda dari standar, dan framework yang lebih dulu memakai decorator, seperti Angular, NestJS dan TypeORM, dibangun di atasnya. Itu sebabnya project NestJS dan TypeORM masih menuntut flag itu.
| Standar (TypeScript 5.0+) | experimentalDecorators: true |
|
|---|---|---|
| Method decorator menerima | (method, context) |
(target, propertyKey, descriptor) |
| Yang di-return | fungsi pengganti | descriptor (descriptor.value diganti) |
| Decorator di parameter constructor | gak ada | ada, misalnya @Inject() |
emitDecoratorMetadata |
gak bisa | bisa |
Versi lama dari logged yang sama:
// tsconfig: "experimentalDecorators": true
function logged(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
const asli = descriptor.value
descriptor.value = function (this: unknown, ...args: unknown[]) {
console.log(`-> ${propertyKey}(${args.join(", ")})`)
const hasil = asli.apply(this, args)
console.log(`<- ${propertyKey} = ${hasil}`)
return hasil
}
return descriptor
}
Hasil di console-nya sama persis. Bedanya cuma apa yang diterima dan dikembalikan decorator-nya.
⚠️ Jebakan yang sering ketemu
- Mencampur dua versi. Decorator versi lama dipakai tanpa flag-nya ditolak:
error TS1241: Unable to resolve signature of method decorator when called as an expression. The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.(disusul TS1270 untuk tipe kembaliannya). Cek dokumentasi library-mu dulu, versi mana yang dia minta. - Mengira decorator jalan di tiap panggilan. Fungsi decorator jalan sekali waktu class dibuat. Kerjaan per panggilan ditaruh di fungsi pengganti yang dia kembalikan.
- Decorator di parameter tanpa flag.
constructor(@Inject("DB") db: string)cuma ada di versi lama. TanpaexperimentalDecorators, hasilnyaerror TS1206: Decorators are not valid here. - Pengganti berupa arrow function. Arrow function gak punya
thissendiri, jadi method yang memakaithisjadi rusak. Pakaifunction.
TL;DR: Decorator adalah fungsi yang ditempel dengan @nama ke class atau method. Versi standar (TypeScript 5.0+, tanpa flag) memberi method decorator dua hal, method aslinya dan context (kind, name, ...), dan yang di-return menggantikan method itu. Fungsi decorator jalan sekali waktu class dibuat; penggantinya yang jalan di tiap panggilan. Versi lama butuh "experimentalDecorators": true, menerima (target, propertyKey, descriptor), dan masih dituntut NestJS dan TypeORM karena formatnya berbeda dari standar. Jangan campur keduanya.