1. Pengenalan Decorators
Decorators adalah fitur experimental di TypeScript yang memungkinkan Anda menambahkan behavior ke class dan anggotanya secara deklaratif. Decorator menggunakan sintaks @expression dan banyak digunakan di framework seperti Angular, NestJS, dan TypeORM.
Decorators pada dasarnya adalah higher-order functions yang menerima target (class, method, property, atau parameter) dan bisa memodifikasi atau menambah behavior pada target tersebut.
Aktivasi Decorators
{
"compilerOptions": {
"target": "ES2021",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"outDir": "./dist"
}
}
// Catatan:
// experimentalDecorators — aktifkan decorator syntax
// emitDecoratorMetadata — aktifkan reflect metadata (perlu reflect-metadata package)
Decorator Execution Order
┌──────────────────────────────────────────────────────────┐
│ DECORATOR EXECUTION ORDER │
│ │
│ 1. Property Decorators (kiri ke kanan) │
│ 2. Parameter Decorators (kiri ke kanan) │
│ 3. Method Decorators (kiri ke kanan) │
│ 4. Class Decorators (bawah ke atas / kanan kiri) │
│ │
│ @ClassDecorator │
│ class MyClass { │
│ @PropertyDecorator │
│ myProp: string; │
│ │
│ @MethodDecorator │
│ myMethod(@ParameterDecorator param: string) {} │
│ } │
│ │
│ Eksekusi: Property → Parameter → Method → Class │
└──────────────────────────────────────────────────────────┘
2. Class Decorators
Class decorator diterapkan pada constructor class. Class decorator bisa mengamati, memodifikasi, atau mengganti class constructor.
Class Decorator Dasar
// Class decorator — menerima constructor sebagai argumen
function Logged<T extends { new (...args: any[]): {} }>(constructor: T) {
console.log(`Class ${constructor.name} diinstansiasi`);
return class extends constructor {
createdAt = new Date();
};
}
@Logged
class User {
constructor(public nama: string, public email: string) {}
}
const user = new User("Budi", "budi@mail.com");
console.log(user);
// Class User diinstansiasi
// User { nama: "Budi", email: "budi@mail.com", createdAt: Date }
// Class decorator yang mengembalikan class baru
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Sealed
class Config {
apiUrl = "https://api.example.com";
timeout = 5000;
}
// ❌ Error: Cannot add property, object is not extensible
// (Config.prototype.newMethod = () => {}) — tidak bisa!
// Class decorator untuk singleton pattern
function Singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
let instance: InstanceType<T>;
return class extends constructor {
constructor(...args: any[]) {
if (instance) {
return instance;
}
super(...args);
instance = this as InstanceType<T>;
}
} as T;
}
@Singleton
class Database {
constructor(public connectionString: string) {
console.log("Database connection created");
}
}
const db1 = new Database("mongodb://localhost:27017");
const db2 = new Database("mongodb://localhost:27017");
// "Database connection created" — hanya sekali!
console.log(db1 === db2); // true — instance yang sama!
3. Method Decorators
Method decorator diterapkan pada method dari class. Method decorator bisa mengamati, memodifikasi, atau mengganti method descriptor.
Method Decorator Dasar
// Method decorator — menerima 3 argumen:
// 1. target (prototype atau constructor)
// 2. propertyKey (nama method)
// 3. descriptor (PropertyDescriptor)
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`📋 Memanggil ${propertyKey}(${JSON.stringify(args)})`);
const result = originalMethod.apply(this, args);
console.log(`✅ ${propertyKey} mengembalikan: ${JSON.stringify(result)}`);
return result;
};
}
class Kalkulator {
@Log
tambah(a: number, b: number): number {
return a + b;
}
@Log
kali(a: number, b: number): number {
return a * b;
}
}
const calc = new Kalkulator();
calc.tambah(5, 3);
// 📋 Memanggil tambah([5,3])
// ✅ tambah mengembalikan: 8
calc.kali(4, 7);
// 📋 Memanggil kali([4,7])
// ✅ kali mengembalikan: 28
Timing Decorator & Memoize
// Timing decorator — mengukur waktu eksekusi method
function Timing(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`⏱️ ${propertyKey} selesai dalam ${(end - start).toFixed(2)}ms`);
return result;
};
}
// Memoize decorator — cache hasil method
function Memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`📦 Cache hit untuk ${propertyKey}`);
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
}
class MathService {
@Timing
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
@Memoize
factorial(n: number): number {
if (n <= 1) return 1;
return n * this.factorial(n - 1);
}
}
const math = new MathService();
console.log(math.factorial(10)); // 3628800
console.log(math.factorial(10)); // 📦 Cache hit — instant!
console.log(math.fibonacci(30));
// ⏱️ fibonacci selesai dalam 12.34ms
Retry Decorator
// Retry decorator — coba ulang jika gagal
function Retry(maxAttempts: number = 3, delay: number = 1000) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
console.log(`⚠️ ${propertyKey} gagal (attempt ${attempt}/${maxAttempts})`);
if (attempt < maxAttempts) {
await new Promise(r => setTimeout(r, delay));
}
}
}
throw lastError;
};
};
}
class ApiService {
private callCount = 0;
@Retry(3, 500)
async fetchData(url: string): Promise<string> {
this.callCount++;
if (this.callCount < 3) {
throw new Error("Network error");
}
return `Data dari ${url}`;
}
}
// Akan otomatis retry 3 kali jika gagal
const api = new ApiService();
// api.fetchData("https://api.example.com").then(console.log);
4. Property Decorators
Property decorator diterapkan pada property (bukan value) dari class. Property decorator menerima 2 argumen: target dan propertyKey.
// Property decorator — menerima 2 argumen:
// 1. target (prototype untuk instance property, constructor untuk static)
// 2. propertyKey (nama property)
function ReadOnly(target: any, propertyKey: string) {
let value: any;
// Override property descriptor
Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: any) {
if (value !== undefined) {
console.warn(`⚠️ ${propertyKey} adalah readonly dan tidak bisa diubah!`);
return;
}
value = newValue;
},
enumerable: true,
configurable: true
});
}
class AppConfig {
@ReadOnly
appName: string = "MyApp";
@ReadOnly
version: string = "1.0.0";
timeout: number = 5000;
}
const config = new AppConfig();
console.log(config.appName); // "MyApp"
config.appName = "NewApp"; // ⚠️ Warning: readonly!
console.log(config.appName); // "MyApp" — tidak berubah!
config.timeout = 10000; // ✅ Bisa diubah
console.log(config.timeout); // 10000
// Required decorator — pastikan property diisi
function Required(target: any, propertyKey: string) {
// Simpan metadata
const requiredProps = Reflect.getMetadata("required", target) || [];
requiredProps.push(propertyKey);
Reflect.defineMetadata("required", requiredProps, target);
}
// Validation function
function validate(obj: any): string[] {
const errors: string[] = [];
const requiredProps = Reflect.getMetadata("required", obj) || [];
for (const prop of requiredProps) {
if (obj[prop] === undefined || obj[prop] === null || obj[prop] === "") {
errors.push(`Property '${prop}' wajib diisi`);
}
}
return errors;
}
class CreateUserDto {
@Required
nama: string = "";
@Required
email: string = "";
bio?: string;
}
const dto = new CreateUserDto();
const errors = validate(dto);
console.log(errors);
// ["Property 'nama' wajib diisi", "Property 'email' wajib diisi"]
dto.nama = "Budi";
dto.email = "budi@mail.com";
console.log(validate(dto)); // [] — tidak ada error!
5. Parameter Decorators
Parameter decorator diterapkan pada parameter method. Parameter decorator menerima 3 argumen: target, propertyKey, dan parameterIndex.
// Parameter decorator — menerima 3 argumen:
// 1. target (prototype)
// 2. propertyKey (nama method)
// 3. parameterIndex (posisi parameter, dimulai dari 0)
function RequiredParam(target: any, propertyKey: string, parameterIndex: number) {
// Simpan metadata parameter yang required
const requiredParams: number[] =
Reflect.getOwnMetadata("requiredParams", target, propertyKey) || [];
requiredParams.push(parameterIndex);
Reflect.defineMetadata("requiredParams", requiredParams, target, propertyKey);
}
// Method decorator yang memvalidasi parameter
function Validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParams: number[] =
Reflect.getOwnMetadata("requiredParams", target, propertyKey) || [];
for (const index of requiredParams) {
if (args[index] === undefined || args[index] === null) {
throw new Error(
`Parameter ke-${index} dari method '${propertyKey}' tidak boleh null/undefined`
);
}
}
return originalMethod.apply(this, args);
};
}
class UserService {
@Validate
createUser(
@RequiredParam nama: string,
email: string,
@RequiredParam umur: number
): object {
return { nama, email, umur };
}
}
const service = new UserService();
// ✅ Valid
console.log(service.createUser("Budi", "budi@mail.com", 25));
// ❌ Error: Parameter ke-0 dari method 'createUser' tidak boleh null/undefined
// service.createUser(null as any, "budi@mail.com", 25);
Inject Decorator
// Metadata keys
const INJECT_METADATA_KEY = Symbol("inject");
// Inject decorator — tandai parameter untuk dependency injection
function Inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
const existingParams: { index: number; token: string }[] =
Reflect.getOwnMetadata(INJECT_METADATA_KEY, target) || [];
existingParams.push({ index: parameterIndex, token });
Reflect.defineMetadata(INJECT_METADATA_KEY, existingParams, target);
};
}
// Simple DI Container
class Container {
private services = new Map<string, any>();
register<T>(token: string, instance: T): void {
this.services.set(token, instance);
}
resolve<T>(constructor: new (...args: any[]) => T): T {
const params: { index: number; token: string }[] =
Reflect.getOwnMetadata(INJECT_METADATA_KEY, constructor) || [];
const args: any[] = [];
for (const { index, token } of params) {
const service = this.services.get(token);
if (!service) {
throw new Error(`Service '${token}' tidak terdaftar`);
}
args[index] = service;
}
return new constructor(...args);
}
}
// Contoh penggunaan
class UserController {
constructor(
@Inject("Logger") private logger: any,
@Inject("Database") private db: any
) {}
getUser(id: number): string {
this.logger.log(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
const container = new Container();
container.register("Logger", { log: (msg: string) => console.log(`[LOG] ${msg}`) });
container.register("Database", { query: (sql: string) => `Result: ${sql}` });
// Resolve otomatis inject dependencies
const controller = container.resolve(UserController);
controller.getUser(1);
// [LOG] Fetching user 1
6. Decorator Factories
Decorator factory adalah fungsi yang mengembalikan decorator. Ini memungkinkan Anda membuat decorator yang configurable dengan parameter.
// Decorator factory — fungsi yang mengembalikan decorator
function Throttle(delayMs: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
let lastCall = 0;
descriptor.value = function (...args: any[]) {
const now = Date.now();
if (now - lastCall < delayMs) {
console.log(`🚫 ${propertyKey} di-throttle, tunggu ${delayMs}ms`);
return;
}
lastCall = now;
return originalMethod.apply(this, args);
};
};
}
// Debounce decorator factory
function Debounce(delayMs: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
let timeoutId: ReturnType<typeof setTimeout>;
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
originalMethod.apply(this, args);
}, delayMs);
};
};
}
class SearchService {
@Debounce(300)
search(query: string): void {
console.log(`🔍 Mencari: "${query}"`);
// API call di sini
}
}
const search = new SearchService();
search.search("a"); // di-cancel
search.search("ab"); // di-cancel
search.search("abc"); // ✅ Ini yang dieksekusi setelah 300ms
// Access control decorator factory
function Authorize(...roles: string[]) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUser = (this as any).currentUser;
if (!currentUser || !roles.includes(currentUser.role)) {
throw new Error(
`🚫 Akses ditolak! Method '${propertyKey}' hanya untuk: ${roles.join(", ")}`
);
}
return originalMethod.apply(this, args);
};
};
}
class AdminPanel {
currentUser = { nama: "Budi", role: "admin" };
@Authorize("admin")
deleteUser(id: number): void {
console.log(`User ${id} dihapus`);
}
@Authorize("admin", "moderator")
banUser(id: number): void {
console.log(`User ${id} di-ban`);
}
@Authorize("user", "admin", "moderator")
viewProfile(id: number): void {
console.log(`Viewing profile ${id}`);
}
}
const panel = new AdminPanel();
panel.deleteUser(1); // ✅ User 1 dihapus
panel.banUser(2); // ✅ User 2 di-ban
7. Decorator Composition
Anda bisa menerapkan beberapa decorator pada target yang sama. Decorator dieksekusi secara tertentu tergantung posisinya.
// Beberapa decorator pada satu method
// Dieksekusi dari bawah ke atas (bottom-up)
function First(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("First decorator evaluated");
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("First: sebelum method");
const result = original.apply(this, args);
console.log("First: sesudah method");
return result;
};
}
function Second(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("Second decorator evaluated");
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("Second: sebelum method");
const result = original.apply(this, args);
console.log("Second: sesudah method");
return result;
};
}
class Example {
@First // dieksekusi kedua (outer)
@Second // dieksekusi pertama (inner)
greet(name: string): string {
return `Hello, ${name}!`;
}
}
const ex = new Example();
ex.greet("Budi");
// Output:
// Second decorator evaluated ← evaluasi duluan
// First decorator evaluated ← evaluasi belakangan
// First: sebelum method ← eksekusi duluan (outer)
// Second: sebelum method ← eksekusi kedua (inner)
// Second: sesudah method ← kembali dari inner
// First: sesudah method ← kembali dari outer
Composable Decorator Pattern
// Compose decorator — gabungkan beberapa decorator menjadi satu
function Compose(
...decorators: ((target: any, propertyKey: string, descriptor: PropertyDescriptor) => void)[]
) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Reverse karena dieksekusi dari bawah ke atas
for (const decorator of decorators.reverse()) {
decorator(target, propertyKey, descriptor);
}
};
}
// Gabungkan beberapa behavior
const CachedAndLogged = Compose(
function (target, key, desc) {
console.log(`Setting up cache for ${key}`);
},
function (target, key, desc) {
console.log(`Setting up logging for ${key}`);
}
);
class DataProcessor {
@CachedAndLogged
process(data: string[]): string[] {
return data.map(d => d.toUpperCase());
}
}
// Practical: Create a comprehensive method decorator
function SafeExecute(options: {
log?: boolean;
retries?: number;
fallback?: any;
} = {}) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
const { log = false, retries = 0, fallback = undefined } = options;
descriptor.value = function (...args: any[]) {
let lastError: Error | undefined;
for (let i = 0; i <= retries; i++) {
try {
if (log) console.log(`[${propertyKey}] Attempt ${i + 1}`);
return original.apply(this, args);
} catch (err) {
lastError = err as Error;
if (log) console.log(`[${propertyKey}] Error: ${lastError.message}`);
}
}
if (fallback !== undefined) return fallback;
throw lastError;
};
};
}
class ExternalService {
@SafeExecute({ log: true, retries: 2, fallback: "default data" })
getData(): string {
if (Math.random() < 0.7) throw new Error("Connection failed");
return "real data";
}
}
8. Praktik: Validation System
Mari kita gabungkan semua jenis decorator untuk membuat sistem validasi yang lengkap dan reusable, mirip seperti yang digunakan di framework seperti class-validator.
// Metadata storage
const VALIDATION_METADATA = Symbol("validation");
interface ValidationRule {
property: string;
type: string;
value?: any;
message?: string;
}
// Helper untuk menyimpan metadata validasi
function addValidation(target: any, property: string, rule: Omit<ValidationRule, "property">) {
const rules: ValidationRule[] =
Reflect.getOwnMetadata(VALIDATION_METADATA, target) || [];
rules.push({ ...rule, property });
Reflect.defineMetadata(VALIDATION_METADATA, rules, target);
}
// ===== Property Decorators =====
function MinLength(min: number) {
return function (target: any, propertyKey: string) {
addValidation(target, propertyKey, {
type: "minLength",
value: min,
message: `${propertyKey} minimal ${min} karakter`
});
};
}
function MaxLength(max: number) {
return function (target: any, propertyKey: string) {
addValidation(target, propertyKey, {
type: "maxLength",
value: max,
message: `${propertyKey} maksimal ${max} karakter`
});
};
}
function IsEmail(target: any, propertyKey: string) {
addValidation(target, propertyKey, {
type: "isEmail",
message: `${propertyKey} harus berupa email valid`
});
}
function IsPositive(target: any, propertyKey: string) {
addValidation(target, propertyKey, {
type: "isPositive",
message: `${propertyKey} harus bilangan positif`
});
}
function Range(min: number, max: number) {
return function (target: any, propertyKey: string) {
addValidation(target, propertyKey, {
type: "range",
value: { min, max },
message: `${propertyKey} harus antara ${min} dan ${max}`
});
};
}
// ===== Validator Engine =====
function validate(instance: any): string[] {
const errors: string[] = [];
const rules: ValidationRule[] =
Reflect.getOwnMetadata(VALIDATION_METADATA, Object.getPrototypeOf(instance)) || [];
for (const rule of rules) {
const value = (instance as any)[rule.property];
switch (rule.type) {
case "minLength":
if (typeof value === "string" && value.length < rule.value) {
errors.push(rule.message!);
}
break;
case "maxLength":
if (typeof value === "string" && value.length > rule.value) {
errors.push(rule.message!);
}
break;
case "isEmail":
if (typeof value === "string" && !value.includes("@")) {
errors.push(rule.message!);
}
break;
case "isPositive":
if (typeof value === "number" && value <= 0) {
errors.push(rule.message!);
}
break;
case "range":
if (typeof value === "number" && (value < rule.value.min || value > rule.value.max)) {
errors.push(rule.message!);
}
break;
}
}
return errors;
}
// ===== Validatable class decorator =====
function Validatable<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
validate(): string[] {
return validate(this);
}
isValid(): boolean {
return this.validate().length === 0;
}
};
}
// ===== Menggunakan Validation System =====
@Validatable
class RegistrationForm {
@MinLength(3)
@MaxLength(50)
nama: string;
@IsEmail
email: string;
@MinLength(8)
password: string;
@Range(17, 100)
umur: number;
constructor(nama: string, email: string, password: string, umur: number) {
this.nama = nama;
this.email = email;
this.password = password;
this.umur = umur;
}
}
// Test
const form1 = new RegistrationForm("Budi", "budi@mail.com", "password123", 25);
console.log((form1 as any).validate()); // [] ✅ Tidak ada error
const form2 = new RegistrationForm("A", "bukan-email", "123", 10);
console.log((form2 as any).validate());
// [
// "nama minimal 3 karakter",
// "email harus berupa email valid",
// "password minimal 8 karakter",
// "umur harus antara 17 dan 100"
// ]
console.log((form1 as any).isValid()); // true
console.log((form2 as any).isValid()); // false
TypeScript Decorators masih dalam tahap experimental (TC39 Stage 3). Syntax dan API bisa berubah di versi mendatang. Untuk production, gunakan library yang sudah stabil seperti class-validator, TypeORM, atau NestJS decorators. Pastikan juga menginstal reflect-metadata untuk metadata reflection.
9. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang TypeScript Decorators: