TYPESCRIPT GENERICS VE CONDITIONAL TYPES

TypeScript açılı parantez generic T konteyneri içine giren ve üç tipli çıkış olarak akan jenerik sembol

Fonksiyon parametresine any yazdığınız an derleyici susuyor, IDE öneri vermiyor, runtime'da bir gün beklenmedik bir undefined ile karşılaşıyorsunuz. Tipler dosyada yazıyor görünüyor ama aslında hiçbir şey doğrulanmıyor — TypeScript'in tüm vaadi bu satırda buharlaşıyor. Çözüm any'yi yasaklamak değil, onu yazdığınız yerlerin çoğunda aslında bir generic parametre veya conditional type istediğinizi fark etmek.

any Neden Tip Güvenliğini Çökertir?

any, tipi bilinmeyen bir değer için değil, tip kontrolünü kapatmak için kullanılır. Derleyici bu değerle yapılan her işlemi onaylar: alanına eriştiniz mi var sayar, fonksiyon olarak çağırdınız mı çağrılabilir sayar. Aşağıdaki kod hatasız derlenir ama çalıştığında çöker:

function getFirst(items: any) {
  return items[0].name.toUpperCase();
}

getFirst(42); // derleyici sessiz, runtime patlar

Problem şu: yazar aslında "bu fonksiyon herhangi bir dizi alır ve içindeki öğelerden birini döner" demek istiyordu. Bunu söyleyebileceği bir araç vardı — sadece kullanmadı. Konuya ilişkin geniş kapsamlı belgeleri ek bir başvuru kaynağı olarak değerlendirilebilir.

Generic Parametre: Tipi Çağıran Söylesin

Generic parametre, bir fonksiyon veya tip tanımına "bu kısmı çağıran kod doldursun" boşluğu açar. Yukarıdaki örneğin tip-güvenli hali:

function getFirst<T>(items: T[]): T {
  return items[0];
}

const user = getFirst([{ name: "Ada" }, { name: "Lin" }]);
// user: { name: string }

T, çağrıldığı yerden çıkarımla doldurulur. Artık getFirst(42) derlenmez çünkü 42 bir T[] değildir. Generic'ler sadece dizi için değil, neredeyse her "içeriği değişen kap" için kullanılır:

  • Constraint ile sınırlama: <T extends { id: string }> diyerek T'nin en azından id alanı taşımasını şart koşarsınız.
  • Birden fazla parametre: map<Input, Output>(arr: Input[], fn: (x: Input) => Output): Output[] hem girişi hem çıkışı izler.
  • Varsayılan değer: <T = string> ile çağıran açıkça yazmazsa kullanılacak tip belirlenir.
  • Çağrı sitesinde manuel verme: getFirst<User>(data) şeklinde çıkarımı geçersiz kılabilirsiniz.

Bu mekanizmaların derinine inmek için TypeScript eğitimi içeriğinden yararlanabilirsiniz; özellikle generic constraint ve keyof kombinasyonu pratikte sık karşınıza çıkacaktır.

Generic T konteynerine üç farklı şekil giriş ve aynı şekiller tipli çıkış inference diyagramı

Conditional Type: Tipe Göre Tip Üret

Generic'ler "tipi dışarıdan al" der; conditional type ise "aldığın tipe göre farklı bir tip dön" der. Sözdizimi terner ifadeye benzer: T extends U ? X : Y.

type Unwrap<T> = T extends Promise<infer U> ? U : T;

type A = Unwrap<Promise<string>>; // string
type B = Unwrap<number>;          // number

infer U, koşulun sol tarafındaki bir alt-tipi yakalayıp sağ tarafta kullanmaya yarar. Bu sayede any'ye düşmeden, gelen tipi soyup içindeki gerçek değeri ortaya çıkarırsınız.

Distributive Conditional Types

Conditional type, union üzerinde çalıştırıldığında her elemana ayrı ayrı uygulanır:

type NonNull<T> = T extends null | undefined ? never : T;
type Clean = NonNull<string | null | number>; // string | number

Bu davranış Exclude, Extract, NonNullable gibi standart utility'lerin temelidir.

Pratik Senaryo: API İstemcisi

Diyelim ki bir HTTP istemcisi yazıyorsunuz. Endpoint adına göre dönen tip değişiyor. any ile yazılsa şöyle olur:

function request(endpoint: string): any { /* ... */ }
const u = request("/users/1"); // u: any, hiçbir koruma yok

Önce endpoint-tip eşleşmesini bir map ile modelleyin, sonra generic ve conditional type'ı birleştirin:

type Routes = {
  "/users/1": { id: number; name: string };
  "/products": { sku: string; price: number }[];
};

function request<K extends keyof Routes>(endpoint: K): Promise<Routes[K]> {
  return fetch(endpoint).then(r => r.json());
}

const u = await request("/users/1");
// u: { id: number; name: string }

Artık endpoint adını yanlış yazarsanız derleme hatası alırsınız, dönen değerin alanları otomatik tamamlanır, refaktör güvenli hale gelir.

Ne Zaman Hangisi?

  1. Generic parametre: Fonksiyon ya da sınıf, içeriği bilmeden çalışacak şekilde tasarlanıyorsa.
  2. Conditional type: Gelen tipe bakıp farklı çıkış tipi üretmek gerekiyorsa.
  3. infer: Mevcut bir tipin içinden bir parçayı ayıklamak gerekiyorsa (örn. ReturnType, Parameters).
  4. Mapped type ile kombinasyon: Bir nesnenin tüm alanlarını koşullu olarak dönüştürmek gerekiyorsa.

Sık Karşılaşılan Tuzaklar

  • Aşırı generic'leşme: İki yerden çağrılan bir fonksiyona 4 generic parametre eklemek okunabilirliği bitirir. Önce somut tipi yazın, ihtiyaç çıkınca soyutlayın.
  • Constraint'siz T: <T> deyip içinde t.name'e erişemezsiniz; extends { name: string } şarttır.
  • Conditional type'ın okunabilirliği: Üst üste iç içe koşullar, başkası bir yana, üç ay sonra siz de okuyamazsınız. Karmaşık koşulları ara type tanımlarına bölün.
  • any yerine unknown: Gerçekten tipi bilinmeyen bir değer için any değil unknown kullanın; unknown üzerinde işlem yapmadan önce daraltma zorunludur.
T extends U ternary elması iki dallı çıkış X ve Y conditional type karar diyagramı

İleri tip sistemini öğrenmenin zor tarafı sözdizimi değil, ne zaman hangi aracı kullanacağınızı sezmek. any yazma isteği geldiğinde durup "burada aslında ne bilmiyorum?" diye sorun: cevap "çağıran söyleyecek" ise generic, "gelen tipe göre değişecek" ise conditional type istiyorsunuz demektir. Birkaç hafta bu refleksle yazılmış kod, hem siz hem de sizi takip edenler için çok daha az hata ayıklama anlamına gelir. Detaylı pratik örnekler ve egzersizler için ilgili eğitim içeriğini inceleyebilirsiniz.