Decorators ve Metadata
Süslemecilik Sanatı
Bir hediye düşün. Hediyenin kendisi değişmiyor — ama üzerine kurdele, şerit, kart ekliyorsun. Hediyenin özü aynı kalıyor, ama etrafına ekstra davranış ve bilgi sarıyorsun. İşte Decorator (dekoratör) kavramı tam olarak bu: bir class'a, metoda veya property'ye dokunmadan onlara ekstra yetenek kazandırma.
Decorator'lar, büyük projelerde tekrar eden "kesişen kaygıları" (cross-cutting concerns) ele alır: loglama, yetkilendirme, caching, validasyon, performans ölçümü... Bunları her fonksiyona elle yazmak yerine, bir dekoratör ile "süslersin."
Bu kavram Python'da @decorator, Java'da @Annotation, C#'da [Attribute] olarak karşına çıkar. TypeScript'te de @decorator syntax'ı ile kullanılır.
⚠️ Dikkat: TypeScript Decorator'lar şu an iki farklı versiyonda var: - Legacy decorators (Stage 2) —
experimentalDecorators: trueile etkinleşir. Angular, NestJS gibi framework'lerde yaygın. - TC39 Stage 3 decorators — TypeScript 5.0+ ile desteklenir,experimentalDecoratorsgerekmez.
>
Bu derste her ikisini de göreceğiz, ama pratikte hangi framework kullanıyorsan onun beklediği versiyonu kullan.
Decorator'ın Temeli: Fonksiyon Sarmalama
Decorator'ları anlamak için önce higher-order function kavramını hatırla: bir fonksiyon alıp, onu sarmalayıp, gelişmiş versiyonunu döndüren fonksiyon.
// Decorator olmadan — manuel sarmalama
function originalGreet(name: string): string {
return `Merhaba ${name}`;
}
// Loglama ekleyelim — ama fonksiyonu değiştirmeden
function withLogging(fn: Function) {
return function (...args: any[]) {
console.log(`Çağrılıyor: ${fn.name}(${args.join(", ")})`);
const result = fn(...args);
console.log(`Sonuç: ${result}`);
return result;
};
}
const loggedGreet = withLogging(originalGreet);
loggedGreet("Ali");
// Çağrılıyor: originalGreet(Ali)
// Sonuç: Merhaba AliDecorator'lar bu pattern'ın TypeScript tarafından desteklenen, class tabanlı hali.
Legacy Class Decorator
Class decorator, bir class'ın tanımını alır ve onu değiştirebilir veya yerine başka bir class döndürebilir. tsconfig.json'da şunu etkinleştirmen gerekir:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Basit Class Decorator
// Class decorator: constructor fonksiyonunu parametre olarak alır
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
console.log(`${constructor.name} sınıfı mühürlendi (sealed).`);
}
@Sealed
class BankAccount {
constructor(public balance: number) {}
deposit(amount: number): void {
this.balance += amount;
}
}
// Artık BankAccount'a yeni property eklenemez
// Object.defineProperty(BankAccount.prototype, "hack", { value: () => {} });
// ❌ TypeError: Cannot define property hack, object is not extensible@Sealed dediğinde TypeScript, BankAccount class'ını Sealed fonksiyonuna parametre olarak geçirir. Bu fonksiyon class'ı alır, Object.seal() uygular — artık class'a yeni property eklenemez.
Constructor'ı Genişletme
// Class'a otomatik timestamp ve id ekle
function WithTimestamp<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
createdAt = new Date();
id = Math.random().toString(36).substring(2, 9);
};
}
@WithTimestamp
class Order {
constructor(public product: string, public quantity: number) {}
}
const order = new Order("Laptop", 2);
console.log((order as any).createdAt); // 2025-03-01T...
console.log((order as any).id); // "a7x9k2m"⚠️ Dikkat: Legacy decorator ile class'ı genişlettiğinde, TypeScript eklenen property'lerin tipini otomatik olarak bilmez.
as anyveya ekstra interface tanımı gerekebilir. TC39 decorator'lar bu sorunu daha iyi çözer.
Method Decorator
Method decorator, bir metodu sarmalayarak ekstra davranış ekler. Üç parametre alır:
target— class'ın prototype'ı (instance method) veya constructor (static method)propertyKey— metodun adıdescriptor—PropertyDescriptornesnesi (metodun get/set/value/writable bilgileri)
// Performans ölçümü decorator'ı
function MeasureTime(
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} çalışma süresi: ${(end - start).toFixed(2)}ms`);
return result;
};
return descriptor;
}
class DataService {
@MeasureTime
processData(items: number[]): number {
// Ağır bir hesaplama simülasyonu
let sum = 0;
for (let i = 0; i < items.length; i++) {
sum += Math.sqrt(items[i]);
}
return sum;
}
@MeasureTime
fetchFromCache(key: string): string {
// Cache'den hızlı okuma
return `cached_${key}`;
}
}
const service = new DataService();
service.processData([1, 4, 9, 16, 25]);
// processData çalışma süresi: 0.03ms
service.fetchFromCache("users");
// fetchFromCache çalışma süresi: 0.01msRetry Decorator — Hata Durumunda Tekrar Dene
function Retry(maxAttempts: number = 3) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
console.warn(
`${propertyKey} - Deneme ${attempt}/${maxAttempts} başarısız: ${lastError.message}`
);
if (attempt < maxAttempts) {
// Exponential backoff: her denemede bekleme süresi artar
const delay = Math.pow(2, attempt) * 100;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError!;
};
return descriptor;
};
}
class ApiClient {
@Retry(3)
async fetchUser(id: number): Promise<{ name: string }> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}Dikkat et: Retry(3) bir Decorator Factory — decorator döndüren bir fonksiyon. Bu sayede parametre alabilir. Birazdan detaylı bakacağız.
Property Decorator
Property decorator, bir class property'sinin tanımını yakalar. Method decorator'a göre daha kısıtlıdır — PropertyDescriptor almaz (çünkü property henüz oluşturulmamıştır).
// Minimum değer kontrolü
function Min(minValue: number) {
return function (target: any, propertyKey: string) {
let value: number;
const getter = () => value;
const setter = (newValue: number) => {
if (newValue < minValue) {
throw new Error(
`${propertyKey} en az ${minValue} olmalı, ${newValue} verildi`
);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
// String uzunluk kontrolü
function MaxLength(max: number) {
return function (target: any, propertyKey: string) {
let value: string;
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newValue: string) => {
if (newValue.length > max) {
throw new Error(
`${propertyKey} en fazla ${max} karakter olabilir`
);
}
value = newValue;
},
enumerable: true,
configurable: true,
});
};
}
class Product {
@MaxLength(100)
name: string;
@Min(0)
price: number;
@Min(0)
stock: number;
constructor(name: string, price: number, stock: number) {
this.name = name;
this.price = price;
this.stock = stock;
}
}
const product = new Product("Laptop", 999, 50); // ✅
const bad = new Product("Phone", -10, 5); // ❌ Error: price en az 0 olmalıDecorator Factory: Parametrik Decorator'lar
Decorator factory, decorator döndüren bir fonksiyon. Bu sayede decorator'a parametre geçirebilirsin. Düşün: bir çerçeve fabrikası — çerçevenin boyutunu, rengini, malzemesini belirleyip üretiyorsun. Fabrika ayarları alıyor, çerçeve üretiyor.
// Basit decorator — parametre almaz
function SimpleLog(target: any, key: string, desc: PropertyDescriptor) {
// ...
}
// Decorator Factory — parametre alır, decorator döndürür
function Log(prefix: string) { // ← factory (dış fonksiyon)
return function ( // ← gerçek decorator (iç fonksiyon)
target: any,
key: string,
desc: PropertyDescriptor
) {
const original = desc.value;
desc.value = function (...args: any[]) {
console.log(`[${prefix}] ${key} çağrıldı`, args);
return original.apply(this, args);
};
};
}
class UserService {
@Log("AUTH")
login(username: string): boolean {
return username === "admin";
}
@Log("DB")
saveUser(name: string): void {
// Veritabanına kaydet...
}
}
const svc = new UserService();
svc.login("admin"); // [AUTH] login çağrıldı ["admin"]
svc.saveUser("Ali"); // [DB] saveUser çağrıldı ["Ali"]Birden Fazla Decorator Zinciri
Bir metoda birden fazla decorator uygulanabilir. Sıralama önemli:
function First() {
console.log("First factory");
return function (target: any, key: string, desc: PropertyDescriptor) {
console.log("First decorator uygulandı");
};
}
function Second() {
console.log("Second factory");
return function (target: any, key: string, desc: PropertyDescriptor) {
console.log("Second decorator uygulandı");
};
}
class Example {
@First()
@Second()
method() {}
}
// Çıktı:
// First factory ← factory'ler yukarıdan aşağıya
// Second factory
// Second decorator uygulandı ← decorator'lar aşağıdan yukarıya
// First decorator uygulandıFactory'ler yukarıdan aşağı çalışır (First → Second), ama decorator'lar aşağıdan yukarı uygulanır (Second → First). Matematikteki fonksiyon bileşimi gibi: First(Second(method)).
Reflect Metadata
reflect-metadata kütüphanesi, decorator'lara metadata (üst veri) ekleme ve okuma yeteneği kazandırır. NestJS, Angular, TypeORM gibi framework'lerin temelini oluşturur.
npm install reflect-metadataimport "reflect-metadata";
// Metadata anahtarları
const ROLES_KEY = "roles";
const ROUTE_KEY = "route";
// Rol gerektiren metotları işaretle
function Roles(...roles: string[]) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata(ROLES_KEY, roles, target, propertyKey);
};
}
// HTTP route tanımla
function Route(path: string, method: string = "GET") {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata(ROUTE_KEY, { path, method }, target, propertyKey);
};
}
class UserController {
@Route("/users", "GET")
@Roles("admin", "editor")
getUsers() {
return ["Ali", "Veli"];
}
@Route("/users", "POST")
@Roles("admin")
createUser() {
// Kullanıcı oluştur...
}
@Route("/users/:id", "GET")
@Roles("admin", "editor", "viewer")
getUser() {
// Tek kullanıcı getir...
}
}
// Metadata'yı oku — framework seviyesinde kullanılır
function getRouteInfo(controller: any, methodName: string) {
const route = Reflect.getMetadata(ROUTE_KEY, controller.prototype, methodName);
const roles = Reflect.getMetadata(ROLES_KEY, controller.prototype, methodName);
return { route, roles };
}
console.log(getRouteInfo(UserController, "getUsers"));
// { route: { path: "/users", method: "GET" }, roles: ["admin", "editor"] }
console.log(getRouteInfo(UserController, "createUser"));
// { route: { path: "/users", method: "POST" }, roles: ["admin"] }emitDecoratorMetadata
tsconfig.json'da emitDecoratorMetadata: true etkinleştirildiğinde, TypeScript otomatik olarak tip bilgilerini metadata olarak kaydeder:
import "reflect-metadata";
class UserService {
findById(id: number): string {
return `User ${id}`;
}
}
class UserController {
// TypeScript, constructor parametresinin tipini metadata olarak kaydeder
constructor(private userService: UserService) {}
}
// design:paramtypes metadata'sını oku
const paramTypes = Reflect.getMetadata("design:paramtypes", UserController);
console.log(paramTypes); // [UserService]
// Bu bilgi ile Dependency Injection sistemi kurulabilir!
// Framework UserController'ı oluştururken otomatik olarak UserService'i enjekte ederBu mekanizma NestJS ve Angular'ın Dependency Injection sisteminin temelidir.
TC39 Stage 3 Decorators (TypeScript 5.0+)
TypeScript 5.0 ile gelen yeni decorator standardı, legacy versiyondan farklı çalışır. experimentalDecorators bayrağı gerekmez ve API daha temizdir.
// TC39 method decorator — yeni syntax
function logged(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`[LOG] ${methodName} çağrıldı`);
const result = originalMethod.call(this, ...args);
console.log(`[LOG] ${methodName} tamamlandı`);
return result;
}
return replacementMethod;
}
// TC39 class decorator
function withId(
value: { new (...args: any[]): {} },
context: ClassDecoratorContext
) {
return class extends value {
id = crypto.randomUUID();
};
}
// Kullanım aynı
@withId
class Task {
@logged
execute(input: string): string {
return `Processed: ${input}`;
}
}Legacy vs TC39 Farkları
| Özellik | Legacy (experimental) | TC39 (Stage 3) |
|---|---|---|
| tsconfig flag | experimentalDecorators: true | Gerekmez |
| Parametre sayısı | 3 (target, key, descriptor) | 2 (value, context) |
| context nesnesi | Yok | ClassMethodDecoratorContext vb. |
| reflect-metadata | Destekler | Henüz standart değil |
| Framework desteği | Angular, NestJS | Yeni projeler |
💡 İpucu: Eğer Angular veya NestJS kullanıyorsan, legacy decorator kullan — bu framework'ler henüz TC39'a geçmedi. Yeni projede framework kullanmıyorsan TC39 tercih et.
Pratik Örnek: Mini Validation Framework
Tüm decorator kavramlarını birleştiren bir mini validasyon framework'ü:
import "reflect-metadata";
const VALIDATIONS_KEY = Symbol("validations");
// Validasyon kuralı arayüzü
interface ValidationRule {
propertyKey: string;
validator: (value: any) => boolean;
message: string;
}
// Validasyon decorator factory'leri
function Required(target: any, propertyKey: string) {
const rules: ValidationRule[] =
Reflect.getMetadata(VALIDATIONS_KEY, target) || [];
rules.push({
propertyKey,
validator: (value) => value !== null && value !== undefined && value !== "",
message: `${propertyKey} zorunludur`,
});
Reflect.defineMetadata(VALIDATIONS_KEY, rules, target);
}
function Range(min: number, max: number) {
return function (target: any, propertyKey: string) {
const rules: ValidationRule[] =
Reflect.getMetadata(VALIDATIONS_KEY, target) || [];
rules.push({
propertyKey,
validator: (value) => value >= min && value <= max,
message: `${propertyKey} ${min}-${max} arasında olmalı`,
});
Reflect.defineMetadata(VALIDATIONS_KEY, rules, target);
};
}
function Pattern(regex: RegExp) {
return function (target: any, propertyKey: string) {
const rules: ValidationRule[] =
Reflect.getMetadata(VALIDATIONS_KEY, target) || [];
rules.push({
propertyKey,
validator: (value) => regex.test(String(value)),
message: `${propertyKey} format geçersiz`,
});
Reflect.defineMetadata(VALIDATIONS_KEY, rules, target);
};
}
// Doğrulama fonksiyonu
function validate(instance: any): { valid: boolean; errors: string[] } {
const rules: ValidationRule[] =
Reflect.getMetadata(VALIDATIONS_KEY, instance) || [];
const errors: string[] = [];
for (const rule of rules) {
const value = instance[rule.propertyKey];
if (!rule.validator(value)) {
errors.push(rule.message);
}
}
return { valid: errors.length === 0, errors };
}
// Kullanım
class RegisterForm {
@Required
@Pattern(/^[a-zA-Z]{2,20}$/)
name: string;
@Required
@Pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
email: string;
@Required
@Range(18, 120)
age: number;
constructor(name: string, email: string, age: number) {
this.name = name;
this.email = email;
this.age = age;
}
}
// Test
const valid = new RegisterForm("Ali", "ali@test.com", 25);
console.log(validate(valid));
// { valid: true, errors: [] }
const invalid = new RegisterForm("", "not-email", 15);
console.log(validate(invalid));
// { valid: false, errors: ["name zorunludur", "name format geçersiz",
// "email format geçersiz", "age 18-120 arasında olmalı"] }Yaygın Hatalar
1. Decorator Sıralamasını Yanlış Anlamak
// Decorator'lar aşağıdan yukarıya uygulanır!
@A
@B
@C
class MyClass {}
// Uygulama sırası: C → B → A
// A(B(C(MyClass)))2. Property Decorator'da this Bağlamı
// ❌ Arrow function kullanma — this kaybolur
function Bad(target: any, key: string) {
const getter = () => value; // this yok!
}
// ✅ Normal function kullan — this doğru bağlanır
function Good(target: any, key: string) {
Object.defineProperty(target, key, {
get() { return this[`_${key}`]; }, // ✅ this doğru
set(val) { this[`_${key}`] = val; }, // ✅ this doğru
});
}3. experimentalDecorators Açmayı Unutmak
// tsconfig.json — legacy decorator kullanacaksan ZORUNLU
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true // reflect-metadata kullanıyorsan
}
}Özet
Decorator, class/method/property'ye dokunmadan ekstra davranış ekler — loglama, validasyon, yetkilendirme gibi kesişen kaygılar için ideal.
Class Decorator: Constructor fonksiyonunu alır, değiştirebilir veya genişletebilir.
Method Decorator:
PropertyDescriptorüzerinden metodu sarmalayarak ekstra mantık ekler.Property Decorator: Property tanımını yakalayarak getter/setter ekler.
Decorator Factory: Parametre alan, decorator döndüren fonksiyon —
@Retry(3),@Roles("admin")gibi.Reflect Metadata: Decorator'larla tip bilgisi ve özel metadata saklama — DI sistemlerinin temeli.
TC39 vs Legacy: Yeni standart daha temiz ama framework desteği sınırlı. Projenin framework'üne göre seç.
AI Asistan
Sorularını yanıtlamaya hazır