Panduan Lengkap Decorators di TypeScript
Pendahuluan
Jika kamu sudah cukup dalam menyelami TypeScript, pasti pernah menemukan si[2D[K
simbol @ yang menempel di atas sebuah class atau method. Itulah **decorat[9D[K
decorator — salah satu fitur paling powerful sekaligus paling sering di[2D[K
disalahpahami di TypeScript.
Decorator bukan sekadar sintaks manis. Di baliknya, ada mekanisme metaprogr[9D[K metaprogramming yang memungkinkan kamu memodifikasi perilaku class, method,[7D[K method, property, hingga parameter tanpa mengubah kode aslinya. Framework[9D[K Framework seperti NestJS, Angular, dan TypeORM sangat bergantung pada fitur[5D[K fitur ini.
Dalam artikel ini, kita akan membedah cara kerja decorator dari dalam — mul[3D[K mulai dari klasifikasinya, cara kerjanya di level JavaScript runtime, hingg[5D[K hingga bagaimana membangun decorator kustom yang benar-benar berguna. Jika [K kamu ingin membangun layanan backend seperti Tokopedia atau Gojek dengan ar[2D[K arsitektur yang bersih dan modular, memahami decorator adalah langkah yang [K wajib.
Catatan: Pastikan
tsconfig.jsonkamu mengaktifkan"experimentalDec[17D[K”experimentalDecorators”: truedan”emitDecoratorMetadata”: true` untuk [K menggunakan fitur ini. Jika belum familiar dengan konfigurasi tsconfig, kam[3D[K kamu bisa baca [Tutorial Konfigurasi tsconfig.json untuk Pemula](/docs/sw/t[18DK 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[2D[K TypeScript pada saat deklarasi, bukan saat runtime eksekusi. Ia menerima in[2D[K informasi tentang target yang didekorasi dan bisa memodifikasi atau menggan[7D[K mengganti target tersebut.
Ada lima jenis decorator di TypeScript:
| Jenis | Target | Argumen yang diterima |
|---|---|---|
| Class Decorator | Constructor class | constructor |
| Method Decorator | Method di class | target, propertyKey, `descri[7D[K |
descriptor | ||
| Property Decorator | Property di class | target, propertyKey |
| Parameter Decorator | Parameter method | target, propertyKey, `pa[3D[K |
parameterIndex | ||
| Accessor Decorator | Getter/setter | target, propertyKey, `descri[7D[K |
descriptor |
Urutan Eksekusi Decorator
Ketika beberapa decorator ditumpuk, urutannya adalah:
- Decorator dievaluasi dari atas ke bawah
- Decorator dieksekusi dari bawah ke atas (bottom-up)
function First(): MethodDecorator {
console.log("First: dievaluasi"); // Komentar: decorator factory dievalua[8D[K
dievaluasi dari atas ke bawah
return function (
target: object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
): void {
console.log("First: dieksekusi"); // Komentar: decorator dieksekusi dar[3D[K
dari bawah ke atas
};
}
function Second(): MethodDecorator {
console.log("Second: dievaluasi"); // Komentar: decorator factory kedua d[1D[K
dievaluasi setelah First
return function (
target: object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
): void {
console.log("Second: dieksekusi"); // Komentar: decorator terdekat deng[4D[K
dengan method dieksekusi lebih dulu
};
}
class Contoh {
@First()
@Second()
method(): void {
console.log("method dipanggil"); // Komentar: ini hanya berjalan saat m[1D[K
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[5D[K 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[1D[K
aman
return function (
_target: object,
key: string | symbol,
descriptor: TypedPropertyDescriptor<T>
): TypedPropertyDescriptor<T> {
const original = descriptor.value!; // Simpan method asli sebelum dibun[5D[K
dibungkus
descriptor.value = function (
this: ThisParameterType<T>,
...args: Parameters<T>
): ReturnType<T> {
console.log(`[${prefix}] Memanggil ${String(key)} dengan args:`, args[4D[K
args);
const result = original.apply(this, args) as ReturnType<T>; // Jalank[6D[K
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[7D[K 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[6D[K
instance untuk class yang didekorasi
return class extends Base {
constructor(...args: any[]) {
if (instance) return instance as this; // Jika sudah ada, kembalikan [K
instance yang sama
super(...args); // Jalankan constructor asli hanya saat instance pert[4D[K
pertama dibuat
instance = this as InstanceType<TBase>; // Simpan instance pertama se[2D[K
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[5D[K seluruh aplikasi.
2. Method Decorator: Logging Otomatis
Method decorator sangat berguna untuk cross-cutting concerns seperti loggin[6D[K 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[4D[K
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<[8D[K
Promise<Result> {
// # Cache key dibuat dari argumen; cocok untuk argumen yang aman di-[3D[K
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[3D[K
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[2D[K
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[6D[K
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.[4D[K
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[10D[K @Cacheable` ini bisa dipakai di ratusan service method tanpa menulis logik[5D[K
logika caching berulang kali.
3. Property Decorator: Validasi Nilai
Property decorator tidak bisa langsung mengubah nilai property, tapi bisa m[1D[K
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 [K
{
let properties = registry.get(target);
// Menyimpan metadata validator per prototype agar tidak bercampur antar [K
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[4D[K
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[5D[K
number`);
}
// Semua validator dari decorator yang ditumpuk dijalankan di satu [K
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) => {[1D[K
{
if (value < minValue) {
throw new RangeError(
`Property "${String(propertyKey)}" harus minimal ${minValue}, tap[3D[K
tapi menerima ${value}`
);
}
});
};
}
function Max(maxValue: number) {
return function (target: object, key: PropertyKey) {
getOrCreateState(target, key).validators.push((value, propertyKey) => {[1D[K
{
if (value > maxValue) {
throw new RangeError(
`Property "${String(propertyKey)}" tidak boleh lebih dari ${maxVa[7D[K
${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[29D[K
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 [K method atau property.
function AccessControl(role: string) {
return function (target: object, key: string | symbol, descriptor: Proper[6D[K
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[6D[K
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 [K
Python adalah fungsi higher-order yang dieksekusi saat definisi fungsi dan [K
mengganti fungsi asli. Decorator TypeScript bekerja pada level class dan [K
memanfaatkan sistem tipe statis serta metadata reflection. TypeScript decor[5D[K
decorator juga memiliki lima varian (class, method, property, accessor, par[3D[K
parameter) sementara Python lebih fleksibel namun tidak ada standarisasi se[2D[K
serupa.
Apakah decorator sudah menjadi standar resmi JavaScript?
Sampai saat ini, decorator di TypeScript menggunakan proposal TC39 Stage 3 [K
(versi lama disebut “legacy decorators”). TypeScript versi terbaru mulai me[2D[K
mendukung decorator Stage 3 dengan opsi "experimentalDecorators": false. [K
Keduanya berbeda secara sintaks dan perilaku, jadi perhatikan versi TypeScr[7D[K
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. [K
Kedua, tambahkan console.log di dalam decorator factory untuk memverifika[11D[K
memverifikasi bahwa decorator dipanggil. Ketiga, perhatikan bahwa property [K
decorator tidak menerima PropertyDescriptor — jika kamu mencoba mengakses[9D[K
mengaksesnya, hasilnya akan undefined. Gunakan Object.defineProperty un[2D[K
untuk memodifikasi property.
Mengapa decorator tidak bisa digunakan pada fungsi biasa (bukan method [K
class)?
Ini adalah batasan desain TypeScript. Decorator hanya bekerja dalam konteks[7D[K
konteks class karena mekanismenya bergantung pada prototype objek dan sis[3D[K
sistem metadata yang terikat pada class. Untuk fungsi biasa, gunakan Higher[6D[K
Higher-Order Function (HOF) sebagai alternatif — konsepnya serupa tapi tanp[4D[K
tanpa sintaks @.
Apakah penggunaan decorator mempengaruhi performa aplikasi?
Decorator dieksekusi sekali saat deklarasi class (load time), bukan set[3D[K setiap kali method dipanggil. Overhead performa biasanya sangat kecil dan d[1D[K dapat diabaikan. Namun, jika decorator method kamu melakukan operasi berat [K (misalnya membuka koneksi baru), itu bisa berdampak. Decorator yang menggun[7D[K menggunakan closure seperti caching justru bisa meningkatkan performa r[1D[K runtime secara signifikan.
Kesimpulan
Decorator adalah alat metaprogramming yang mengangkat TypeScript ke level y[1D[K
yang jauh lebih ekspresif. Dengan memahami cara kerja class decorator, meth[4D[K
method decorator, property decorator, dan integrasi reflect-metadata, kam[3D[K
kamu bisa membangun framework mini sendiri, sistem validasi yang elegan, at[2D[K
atau arsitektur yang bersih tanpa mengulang kode yang sama.
Kunci utamanya: decorator adalah fungsi biasa yang dieksekusi saat dekl[4D[K deklarasi. Begitu kamu memahami ini, seluruh pola lainnya akan terasa logis[5D[K logis dan mudah dikembangkan.
Selamat belajar dan terus eksplorasi! Jika kamu tertarik mendalami TypeScri[8D[K TypeScript lebih jauh, jangan ragu untuk menjelajahi artikel-artikel lain d[1D[K di KamusNgoding — masih banyak topik seru yang menunggumu.