İçeriğe geç
TypeScript

TypeScript Generics: Esnek ve Yeniden Kullanılabilir Kod

Tarık Tunç
TypeScript Generics: Esnek ve Yeniden Kullanılabilir Kod

TypeScript Generics: Esnek ve Yeniden Kullanılabilir Kod

Generics, TypeScript'in en güçlü özelliklerinden biridir. Tip güvenliğini korurken yeniden kullanılabilir, esnek bileşenler oluşturmanızı sağlar. Bu yazıda generics kavramını temellerden ileri seviyeye kadar kapsamlı bir şekilde inceleyeceğiz.

Generics Nedir ve Neden Gereklidir?

Generics olmadan, bir fonksiyonu farklı tipler için kullanmak istediğinizde ya tip güvenliğinden vazgeçmeniz ya da aynı fonksiyonu her tip için tekrar yazmanız gerekir:

// Problem: any kullanmak tip güvenliğini ortadan kaldırır
function ilkElemaniGetir(dizi: any[]): any {
  return dizi[0];
}

const sayi = ilkElemaniGetir([1, 2, 3]); // any - tip bilgisi kayboldu!
const isim = ilkElemaniGetir(["Ali", "Ayşe"]); // any

// Çözüm: Generics
function ilkElemaniGetirG<T>(dizi: T[]): T {
  return dizi[0];
}

const sayi2 = ilkElemaniGetirG([1, 2, 3]); // number
const isim2 = ilkElemaniGetirG(["Ali", "Ayşe"]); // string
const obj = ilkElemaniGetirG([{ id: 1 }, { id: 2 }]); // { id: number }

Generic Fonksiyonlar

Generic fonksiyonlar, çağrıldıkları andaki tiplerle çalışan esnek fonksiyonlardır:

// Basit generic fonksiyon
function kimlik<T>(deger: T): T {
  return deger;
}

// Tip çıkarımı otomatik çalışır
const sonuc1 = kimlik("merhaba"); // string
const sonuc2 = kimlik(42);         // number

// Açık tip belirtme
const sonuc3 = kimlik<string>("dünya");

// Birden fazla generic parametre
function ciftOlustur<T, U>(ilk: T, ikinci: U): [T, U] {
  return [ilk, ikinci];
}

const cift = ciftOlustur("isim", 42); // [string, number]

// Generic arrow function
const tekrarla = <T>(deger: T, adet: number): T[] => {
  return Array(adet).fill(deger);
};

// Async generic fonksiyon
async function veriGetir<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json() as Promise<T>;
}

interface Kullanici {
  id: number;
  isim: string;
}

const kullanici = await veriGetir<Kullanici>("/api/kullanici/1");

Generic Constraints

Generic tipleri kısıtlayarak, belirli özelliklere sahip tipleri zorunlu kılabilirsiniz:

// extends ile kısıtlama
function uzunlukGetir<T extends { length: number }>(deger: T): number {
  return deger.length;
}

uzunlukGetir("merhaba");  // Geçerli - string'in length'i var
uzunlukGetir([1, 2, 3]);  // Geçerli - array'in length'i var
// uzunlukGetir(42);       // HATA - number'ın length'i yok

// keyof ile kısıtlama
function degerGetir<T, K extends keyof T>(obj: T, anahtar: K): T[K] {
  return obj[anahtar];
}

const kullanici = { isim: "Tarik", yas: 28, email: "tarik@example.com" };
const isim = degerGetir(kullanici, "isim");  // string
const yas = degerGetir(kullanici, "yas");    // number
// degerGetir(kullanici, "adres");            // HATA! "adres" geçerli bir anahtar değil

// Birden fazla kısıtlama
interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(): void;
}

function isleVeKaydet<T extends Serializable & Loggable>(item: T): string {
  item.log();
  return item.serialize();
}
İpucu: Generic constraint'ler, tip güvenliğini korurken esneklik sağlar. extends anahtar kelimesiyle tipleri kısıtlayarak, generic fonksiyonlarınızda belirli özelliklere güvenle erişebilirsiniz.

Generic Interface ve Type

// Generic interface
interface ApiCevap<T> {
  data: T;
  basarili: boolean;
  mesaj: string;
  zaman: number;
}

interface KullaniciListeCevap extends ApiCevap<Kullanici[]> {
  toplamSayfa: number;
  mevcutSayfa: number;
}

// Generic type
type Sonuc<T, E = Error> =
  | { basarili: true; deger: T }
  | { basarili: false; hata: E };

function bolme(a: number, b: number): Sonuc<number, string> {
  if (b === 0) {
    return { basarili: false, hata: "Sıfıra bölme hatası!" };
  }
  return { basarili: true, deger: a / b };
}

// Generic class
class Koleksiyon<T> {
  private elemanlar: T[] = [];

  ekle(eleman: T): void {
    this.elemanlar.push(eleman);
  }

  getir(index: number): T | undefined {
    return this.elemanlar[index];
  }

  filtrele(kosul: (eleman: T) => boolean): T[] {
    return this.elemanlar.filter(kosul);
  }

  haritala<U>(donustur: (eleman: T) => U): U[] {
    return this.elemanlar.map(donustur);
  }
}

const sayilar = new Koleksiyon<number>();
sayilar.ekle(1);
sayilar.ekle(2);
sayilar.ekle(3);

const ciftSayilar = sayilar.filtrele(n => n % 2 === 0); // number[]
const metinler = sayilar.haritala(n => n.toString());    // string[]

Generic ile Design Pattern'ler

// Repository Pattern
interface Repository<T extends { id: number }> {
  getirHepsi(): Promise<T[]>;
  getirById(id: number): Promise<T | null>;
  olustur(veri: Omit<T, "id">): Promise<T>;
  guncelle(id: number, veri: Partial<T>): Promise<T>;
  sil(id: number): Promise<boolean>;
}

class KullaniciRepository implements Repository<Kullanici> {
  async getirHepsi(): Promise<Kullanici[]> {
    return fetch("/api/kullanicilar").then(r => r.json());
  }

  async getirById(id: number): Promise<Kullanici | null> {
    return fetch(`/api/kullanicilar/${id}`).then(r => r.json());
  }

  async olustur(veri: Omit<Kullanici, "id">): Promise<Kullanici> {
    return fetch("/api/kullanicilar", {
      method: "POST",
      body: JSON.stringify(veri)
    }).then(r => r.json());
  }

  async guncelle(id: number, veri: Partial<Kullanici>): Promise<Kullanici> {
    return fetch(`/api/kullanicilar/${id}`, {
      method: "PATCH",
      body: JSON.stringify(veri)
    }).then(r => r.json());
  }

  async sil(id: number): Promise<boolean> {
    const response = await fetch(`/api/kullanicilar/${id}`, { method: "DELETE" });
    return response.ok;
  }
}
Uyarı: Generic parametrelerinizi anlamlı isimlerle adlandırın. T, U, V gibi tek harfli isimler basit durumlar için uygundur, ancak karmaşık generics'lerde TData, TResponse, TError gibi açıklayıcı isimler kullanın.

Default Generic Tipleri

// Default generic parametreler
interface FetchSecenekleri<T = unknown> {
  url: string;
  method?: "GET" | "POST" | "PUT" | "DELETE";
  body?: T;
  headers?: Record<string, string>;
}

// T belirtilmezse unknown kullanılır
const getOptions: FetchSecenekleri = {
  url: "/api/veri"
};

// T açıkça belirtilir
const postOptions: FetchSecenekleri<{ isim: string }> = {
  url: "/api/kullanici",
  method: "POST",
  body: { isim: "Tarik" }
};
Bilgi: Default generic tipler, fonksiyon parametrelerindeki default değerlere benzer şekilde çalışır. Generic kullanımını basitleştirirken, gerektiğinde özelleştirme imkanı sunar.

Sonuç

Generics, TypeScript'te tip güvenliğini korurken kod tekrarını minimize etmenin anahtarıdır. Basit generic fonksiyonlardan başlayarak, constraint'ler, default tipler ve design pattern'ler ile daha karmaşık yapılar oluşturabilirsiniz. Generics'i etkin kullanmak, profesyonel TypeScript kodunun olmazsa olmazıdır.

Kaynaklar

İlgili Yazılar