☀️Siang
Python

🎨 TypeScript Decorators: Panduan Lengkap

Pelajari TypeScript Decorators — class decorators, method decorators, property decorators, parameter decorators, dan penggunaan praktisnya

Artikel: Typescript Decorators Artikel: Typescript Decorators


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

JSON — tsconfig.json
{
  "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

Diagram: Urutan Eksekusi Decorator

📝 Deklarasi Class

🔽 Decorator 3 Atas

🔽 Decorator 2 Tengah

🔽 Decorator 1 Dekat Class

🏗️ Eksekusi Bottom-Up

🔄 Decorator 1 → 2 → 3

✅ Class Ter-dekorasi



2. Class Decorators

Class decorator diterapkan pada constructor class. Class decorator bisa mengamati, memodifikasi, atau mengganti class constructor.

Class Decorator Dasar

TypeScript — Class Decorator
// 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

TypeScript — Method Decorator
// 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

TypeScript — Timing & 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

TypeScript — 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.

TypeScript — Property Decorator
// 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.

TypeScript — Parameter Decorator
// 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

TypeScript — 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.

TypeScript — Decorator Factories
// 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.

TypeScript — Decorator Composition
// 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

TypeScript — Composable 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.

TypeScript — Validation System
// 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
⚠️ Catatan Penting

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:

Pertanyaan 1: Apa fungsi dari experimentalDecorators di tsconfig.json?

a) Mengaktifkan fitur Generics
b) Mengaktifkan decorator syntax (@decorator)
c) Mengaktifkan strict mode
d) Mengaktifkan ES modules

Pertanyaan 2: Apa yang diterima oleh method decorator sebagai argumen?

a) Hanya constructor class
b) target, propertyKey, dan descriptor
c) Hanya nama method
d) target dan parameterIndex

Pertanyaan 3: Apa itu decorator factory?

a) Class khusus untuk membuat decorator
b) Fungsi yang mengembalikan decorator, memungkinkan parameterisasi
c) Interface untuk decorator
d) Library untuk membuat decorator

Pertanyaan 4: Dalam urutan apa @First @Second dieksekusi pada method?

a) @First duluan, @Second kedua
b) @Second duluan, @First kedua (bottom-up evaluation)
c) Keduanya bersamaan
d) Bergantung pada urutan di file

Pertanyaan 5: Apa yang dilakukan emitDecoratorMetadata di tsconfig.json?

a) Menyembunyikan metadata decorator
b) Menghasilkan type metadata otomatis untuk decorator (perlu reflect-metadata)
c) Mengkompilasi decorator ke JavaScript
d) Menghapus decorator saat compile
🔍 Zoom
100%
🎨 Tema