Daftar Isi

Mengenal TypeScript: Decorators

This article has been translated into English version: Learn TypeScript: Decorators

Decorators adalah fitur eksperimental di TypeScript yang memungkinkan kita untuk menambahkan anotasi dan metadata ke kelas, metode, properti, dan parameter. Artikel ini akan menjelaskan konsep, sintaks, dan penggunaan praktis dari Decorators di TypeScript.

Perlu diingat bahwa karena decorators masih merupakan fitur eksperimental, sintaks dan perilakunya mungkin berubah di masa depan. Oleh karena itu, pastikan Anda selalu memeriksa dokumentasi TypeScript terbaru untuk informasi terkini tentang decorators. Anda dapat mengecek status dari decorators di sini: https://github.com/tc39/proposal-decorators.

Apa itu Decorators?

Decorators adalah bentuk khusus dari deklarasi yang dapat dilampirkan ke deklarasi kelas, metode, accessor, properti, atau parameter. Decorators menggunakan sintaks @expression, di mana expression harus mengevaluasi fungsi yang akan dipanggil saat runtime dengan informasi tentang deklarasi yang didekorasi.

Catatan: Decorators masih merupakan fitur eksperimental di TypeScript. Untuk menggunakannya, Anda perlu mengaktifkan flag experimentalDecorators di file tsconfig.json:

{  
  "compilerOptions": {  
    "experimentalDecorators": true  
  }  
}  

Jenis-jenis Decorators

TypeScript mendukung beberapa jenis decorator:

  1. Class Decorators
  2. Method Decorators
  3. Property Decorators
  4. Parameter Decorators
  5. Accessor Decorators

Mari kita bahas masing-masing jenis ini.

Class decorator diterapkan pada deklarasi kelas dan dapat digunakan untuk mengamati, memodifikasi, atau mengganti definisi kelas.

/** Class decorator function */
function Logger(target: Function) {
  console.log(`Class ${target.name} telah dibuat`);
}

/** Menggunakan class decorator */
@Logger
class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const person = new Person("John"); // Output: "Class Person telah dibuat"

Class decorator juga bisa mengembalikan kelas baru yang memperluas kelas asli:

/** Class decorator yang mengembalikan kelas baru */
function WithTimestamp<T extends { new (...args: any[]): {} }>(target: T) {
  return class extends target {
    createdAt = new Date();

    getTimestamp() {
      return this.createdAt;
    }
  };
}

/** Menggunakan class decorator */
@WithTimestamp
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const user = new User("Alice");
console.log(user.name);                 // "Alice"
console.log((user as any).getTimestamp()); // Menampilkan timestamp saat ini

Method decorator diterapkan pada metode kelas dan dapat digunakan untuk mengamati, memodifikasi, atau mengganti definisi metode.

/** Method decorator function */
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    console.log(`Memanggil ${propertyKey} dengan argumen: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Hasil dari ${propertyKey}: ${result}`);
    return result;
  };

  return descriptor;
}

class Calculator {
  /** Menggunakan method decorator */
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(5, 3);
// Output:
// "Memanggil add dengan argumen: [5,3]"
// "Hasil dari add: 8"

Property decorator diterapkan pada properti kelas dan dapat digunakan untuk mengamati atau memodifikasi properti.

/** Property decorator function */
function Format(formatString: string) {
  return function(target: any, propertyKey: string) {
    let value: any;

    const getter = function() {
      return value;
    };

    const setter = function(newValue: any) {
      value = formatString.replace("%s", newValue);
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Greeting {
  /** Menggunakan property decorator */
  @Format("Hello, %s!")
  name: string;
}

const greeting = new Greeting();
greeting.name = "World";
console.log(greeting.name); // "Hello, World!"

Parameter decorator diterapkan pada parameter metode atau konstruktor dan dapat digunakan untuk mengamati parameter.

/** Parameter decorator function */
function Required(target: any, propertyKey: string, parameterIndex: number) {
  const requiredParams: number[] = Reflect.getMetadata("required", target, propertyKey) || [];
  requiredParams.push(parameterIndex);
  Reflect.defineMetadata("required", requiredParams, target, propertyKey);
}

/** Method decorator untuk validasi */
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    const requiredParams: number[] = Reflect.getMetadata("required", target, propertyKey) || [];

    for (const index of requiredParams) {
      if (args[index] === undefined || args[index] === null) {
        throw new Error(`Parameter at index ${index} is required`);
      }
    }

    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class UserService {
  /** Menggunakan parameter decorator dan method decorator */
  @Validate
  createUser(name: string, @Required email: string, age?: number) {
    return { name, email, age };
  }
}

const userService = new UserService();
userService.createUser("John", "john@example.com", 30); // OK
// userService.createUser("Alice", null); // Error: Parameter at index 1 is required

Catatan: Contoh di atas menggunakan reflect-metadata, yang perlu diinstal dan diimpor:

npm install reflect-metadata  
import "reflect-metadata";  

Accessor decorator diterapkan pada accessor properti (getter/setter) dan dapat digunakan untuk mengamati, memodifikasi, atau mengganti definisi accessor.

/** Accessor decorator function */
function Enumerable(value: boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
    return descriptor;
  };
}

class Person {
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  /** Menggunakan accessor decorator */
  @Enumerable(false)
  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

const person = new Person("John");
console.log(person.name); // "John"
console.log(Object.keys(person)); // [] (name tidak termasuk karena enumerable: false)

Decorator Factories

Decorator factory adalah fungsi yang mengembalikan decorator. Ini memungkinkan kita untuk menyesuaikan bagaimana decorator diterapkan pada deklarasi.

/** Decorator factory */
function Log(prefix: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
      console.log(`${prefix} Memanggil ${propertyKey}`);
      const result = originalMethod.apply(this, args);
      console.log(`${prefix} Hasil: ${result}`);
      return result;
    };

    return descriptor;
  };
}

class MathUtils {
  /** Menggunakan decorator factory */
  @Log("[DEBUG]")
  square(n: number): number {
    return n * n;
  }

  @Log("[INFO]")
  cube(n: number): number {
    return n * n * n;
  }
}

const math = new MathUtils();
math.square(3); // "[DEBUG] Memanggil square", "[DEBUG] Hasil: 9"
math.cube(3);   // "[INFO] Memanggil cube", "[INFO] Hasil: 27"

Urutan Evaluasi Decorator

Ketika beberapa decorator diterapkan pada satu deklarasi, evaluasi mereka mengikuti aturan berikut:

  1. Ekspresi decorator dievaluasi dari atas ke bawah.
  2. Hasil evaluasi dipanggil sebagai fungsi dari bawah ke atas.
/** Decorator factories */
function First() {
  console.log("First decorator factory");
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("First decorator called");
  };
}

function Second() {
  console.log("Second decorator factory");
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Second decorator called");
  };
}

class Example {
  /** Menggunakan multiple decorators */
  @First()
  @Second()
  method() {}
}

// Output:
// "First decorator factory"
// "Second decorator factory"
// "Second decorator called"
// "First decorator called"

Kasus Penggunaan Praktis

Berikut ini beberapa kasus penggunaan praktis dari decorators:

/** Method decorator untuk logging dan profiling */
function Profile(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    console.log(`Memanggil ${propertyKey}`);
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} selesai dalam ${end - start} ms`);
    return result;
  };

  return descriptor;
}

class DataService {
  /** Menggunakan decorator profiling */
  @Profile
  fetchData() {
    // Simulasi operasi yang membutuhkan waktu
    const startTime = Date.now();
    while (Date.now() - startTime < 1000) {
      // Menunggu 1 detik
    }
    return { data: "Some data" };
  }
}

const service = new DataService();
service.fetchData();
// Output:
// "Memanggil fetchData"
// "fetchData selesai dalam 1005.67 ms"
/** Decorator untuk dependency injection */
const services: Map<string, any> = new Map();

function Service(name: string) {
  return function<T extends { new (...args: any[]): {} }>(constructor: T) {
    services.set(name, new constructor());
    return constructor;
  };
}

function Inject(serviceName: string) {
  return function(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      get: () => services.get(serviceName),
      enumerable: true,
      configurable: true
    });
  };
}

/** Service implementation */
@Service("loggerService")
class LoggerService {
  log(message: string) {
    console.log(`[LOG]: ${message}`);
  }
}

@Service("userService")
class UserService {
  getUser(id: number) {
    return { id, name: "John" };
  }
}

/** Client class using injected services */
class AppComponent {
  @Inject("loggerService")
  private logger: LoggerService;

  @Inject("userService")
  private userService: UserService;

  run() {
    this.logger.log("Application started");
    const user = this.userService.getUser(1);
    this.logger.log(`User loaded: ${user.name}`);
  }
}

const app = new AppComponent();
app.run();
// Output:
// "[LOG]: Application started"
// "[LOG]: User loaded: John"
/** Decorator untuk validasi */
function Min(limit: number) {
  return function(target: any, propertyKey: string) {
    let value: any;

    const getter = function() {
      return value;
    };

    const setter = function(newValue: any) {
      if (newValue < limit) {
        throw new Error(`Value cannot be less than ${limit}`);
      }
      value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

function Max(limit: number) {
  return function(target: any, propertyKey: string) {
    let value: any;

    const getter = function() {
      return value;
    };

    const setter = function(newValue: any) {
      if (newValue > limit) {
        throw new Error(`Value cannot be greater than ${limit}`);
      }
      value = newValue;
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Product {
  @Min(0)
  @Max(1000)
  price: number;

  constructor(price: number) {
    this.price = price;
  }
}

const validProduct = new Product(100);   // OK
// const invalidProduct1 = new Product(-10);  // Error: Value cannot be less than 0
// const invalidProduct2 = new Product(2000); // Error: Value cannot be greater than 1000

Kesimpulan

Decorators adalah fitur yang kuat di TypeScript yang memungkinkan kita untuk menambahkan metadata dan perilaku tambahan ke kelas, metode, properti, dan parameter. Meskipun masih eksperimental, decorators sudah banyak digunakan dalam framework dan library populer seperti Angular, NestJS, dan TypeORM.

Dengan memahami dan memanfaatkan decorators, kita dapat menulis kode yang lebih deklaratif, reusable, dan ekspresif. Decorators memungkinkan kita untuk menerapkan aspek-aspek seperti logging, validasi, dan dependency injection dengan cara yang bersih dan terpisah dari logika bisnis utama.

Namun, perlu diingat bahwa karena decorators masih merupakan fitur eksperimental, sintaks dan perilakunya mungkin berubah di masa depan. Selalu periksa dokumentasi TypeScript terbaru untuk informasi terkini tentang decorators.

Konten Terkait