Decorator adalah fungsi yang kamu pasang pada class atau member class dengan @name. Fungsi ini menerima method aslinya (atau class, atau field) ditambah objek context yang mendeskripsikannya, dan bisa mengembalikan penggantinya. TypeScript mendukung decorator standar tanpa flag compiler apa pun.
@logged dijalankan sekali, saat class didefinisikan, dan mengganti add dengan wrapper yang dikembalikannya. Setiap pemanggilan melewati wrapper itu. Parameter generic menjaga tipe this, argumen, dan nilai kembalian method tetap utuh, jadi add tetap menerima dua angka dan mengembalikan angka.
Bagaimana Decorator Dikompilasi
Decorator standar berasal dari proposal TC39 untuk JavaScript, dan TypeScript sudah mengimplementasikannya sejak TypeScript 5.0. Proposal itu belum menjadi bagian dari standar JavaScript, dan Node 24 tidak bisa mem-parse sintaks @, jadi ketika target-nya ES2022 (seperti di halaman-halaman ini) compiler menulis ulang setiap class yang didekorasi menjadi JavaScript biasa yang memanggil fungsi helper (__esDecorate dan __runInitializers, yang ditulis di bagian atas file). Output-nya berjalan di mana pun ES2022 berjalan.
Satu konsekuensinya: file dengan decorator tidak bisa dijalankan dengan type stripping bawaan Node (node file.ts), yang hanya membuang tipe dan membiarkan @ di tempatnya. Node berhenti dengan SyntaxError: Invalid or unexpected token. Kompilasi dulu dengan tsc atau bundler.
Jenis Decorator dan Signature-nya
Setiap decorator standar berbentuk (value, context) => replacement | void. Apa itu value, dan apa yang boleh kamu kembalikan, bergantung pada apa yang didekorasi:
| Mendekorasi | value | Tipe context | Nilai kembalian |
|---|---|---|---|
| class | class itu sendiri | ClassDecoratorContext | class pengganti, atau tidak ada |
| method | method itu | ClassMethodDecoratorContext | method pengganti |
| getter / setter | getter atau setter itu | ClassGetterDecoratorContext / ClassSetterDecoratorContext | getter atau setter pengganti |
| field | undefined | ClassFieldDecoratorContext | fungsi yang memetakan nilai awal |
field accessor | { get, set } | ClassAccessorDecoratorContext | { get?, set?, init? } |
Setiap objek context punya kind, name, dan addInitializer. Context sebuah member class juga punya static, private, dan objek access untuk membaca member itu dari sebuah instance. Decorator juga bisa dipakai pada member static dan #private.
Decorator Factory
Untuk mengirim opsi, tulis fungsi yang mengembalikan decorator, lalu panggil di tempat @. Inilah decorator factory:
@retry(3) memanggil retry lebih dulu, dan fungsi yang dikembalikannya adalah decorator yang sebenarnya. Beberapa decorator bisa ditumpuk: di @a @b method(), b diterapkan lebih dulu lalu a membungkus hasilnya.
Class Decorator dan addInitializer
Class decorator menerima class itu sendiri. Decorator ini bisa mengembalikan subclass untuk menggantikannya, atau tidak mengembalikan apa pun dan hanya mencatatnya di suatu tempat. context.addInitializer mendaftarkan kode yang dijalankan pada saat tertentu: untuk class decorator, tepat setelah class selesai didefinisikan; untuk method decorator, saat setiap instance dibuat.
Class decorator ditulis di atas class (atau setelah export). Tanpa @bound, memanggil loose() akan melempar error, karena this akan bernilai undefined.
Field Decorator dan Accessor Decorator
Field decorator tidak bisa melihat atau mencegat penugasan berikutnya: value-nya adalah undefined, dan yang bisa dikembalikannya hanyalah fungsi yang mengubah nilai awal field. Untuk mencegat pembacaan dan penulisan, deklarasikan field dengan kata kunci accessor, yang mengubahnya menjadi pasangan getter dan setter dengan penyimpanan private, lalu dekorasi field itu:
accessor adalah bagian dari proposal yang sama. Kata kunci ini menghasilkan getter dan setter sungguhan di atas field #private, itulah sebabnya p.price = -5 melewati set milik decorator.
Decorator Standar vs experimentalDecorators Legacy
Sebelum TypeScript 5.0, satu-satunya decorator yang dimiliki TypeScript adalah versi awal dari proposal itu, yang dinyalakan dengan experimentalDecorators. Flag itu masih ada, dan ia mengalihkan compiler ke model legacy, dengan signature dan semantik yang berbeda:
| Standar (tanpa flag) | Legacy (experimentalDecorators) | |
|---|---|---|
| Signature | (value, context) | (target, propertyKey, descriptor) |
| Cara mengubah method | mengembalikan fungsi baru | mengubah descriptor.value |
| Parameter decorator | tidak didukung (TS1206) | didukung |
emitDecoratorMetadata | tidak didukung | didukung (info tipe saat runtime lewat reflect-metadata) |
Decorator accessor ({ get, set, init }), addInitializer | ya | tidak |
| Berdasarkan | proposal TC39 | draf lamanya |
Decorator yang ditulis untuk satu model tidak lolos pemeriksaan tipe di model lainnya. Berikut decorator bergaya legacy di proyek tanpa flag itu:
index.ts(11,5): 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.
Perbaikannya adalah menulis ulang dalam bentuk standar (value, context), seperti contoh pertama di halaman ini, atau menyalakan model legacy untuk seluruh proyek:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Angular, NestJS, dan TypeORM masih mengatur experimentalDecorators di config yang dihasilkan alat mereka dan diminta dokumentasinya. NestJS dan TypeORM juga butuh emitDecoratorMetadata, karena keduanya membaca tipe saat runtime: NestJS untuk meng-inject parameter constructor, TypeORM untuk memetakan property ke kolom:
// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
constructor(@Inject(DB) private db: Database) {}
}
Kalau kamu memakai salah satu framework tersebut, tulis decorator dengan cara legacy dan ikuti dokumentasinya. Untuk kode baru tanpa framework semacam itu, pakai decorator standar.
Kapan Memakai Decorator
Decorator cocok untuk perilaku lintas bagian yang kalau tidak akan diulang di banyak method: logging, pengukuran waktu, caching, retry, pemeriksaan akses, validasi, dan mendaftarkan class ke container atau router. Decorator juga menyembunyikan alur kontrol, karena pembaca harus mencari tahu apa yang dilakukan @retry sebelum tahu apa yang dilakukan method itu. Untuk satu atau dua pemakaian, higher-order function biasa (const fetchData = retry(3, rawFetch)) lebih sederhana dan juga bisa dipakai di luar class.
Pertanyaan yang sering diajukan
Apa itu decorator di TypeScript?
Decorator adalah fungsi yang diterapkan pada class atau member class dengan sintaks @name. Fungsi ini menerima hal yang didekorasi dan sebuah objek context, dan bisa mengembalikan penggantinya: method yang dibungkus, class baru, atau fungsi yang mengubah nilai awal sebuah field. Pemakaian yang umum adalah logging, validasi, caching, dan mendaftarkan class.
Apakah aku butuh experimentalDecorators untuk memakai decorator di TypeScript?
Tidak. Sejak TypeScript 5.0, decorator standar (TC39) berjalan tanpa flag. experimentalDecorators mengalihkan compiler ke model decorator lama (legacy), yang menjadi dasar framework seperti Angular dan NestJS. Keduanya memakai signature fungsi yang berbeda dan tidak bisa saling menggantikan.
Apa bedanya decorator standar dan experimentalDecorators?
Decorator standar menerima (value, context) dan mengembalikan pengganti. Decorator legacy menerima (target, propertyKey, descriptor) dan mengubah property descriptor. Hanya model legacy yang mendukung parameter decorator dan emitDecoratorMetadata; hanya model standar yang punya decorator accessor yang mengembalikan { get, set, init } dan context.addInitializer.
Apakah TypeScript mendukung parameter decorator?
Hanya kalau experimentalDecorators dinyalakan. Di mode standar, decorator pada parameter menghasilkan error TS1206: Decorators are not valid here, karena proposal TC39 tidak mencakup parameter decorator. Karena itu, framework dependency injection yang mendekorasi parameter constructor butuh flag legacy tersebut.
Dalam urutan apa beberapa decorator diterapkan?
Ekspresi decorator dievaluasi dari atas ke bawah, tetapi diterapkan dari bawah ke atas: di @a @b method(), b membungkus method lebih dulu lalu a membungkus hasilnya. Jadi a berjalan paling luar saat method dipanggil.