Mert Özen Her Satırda Daha İleri
Frontend TypeScript Orta — #02

TypeScript'te Generic Constraint'ler: extends, Varsayılan Tipler, const ve NoInfer

Mert Özen 3 Kas 2026 7 dk 1 görüntülenme
TypeScript'te Generic Constraint'ler: extends, Varsayılan Tipler, const ve NoInfer

Generic tip parametrelerine extends ile kısıt koymayı, keyof ile iki parametreyi bağlamayı, varsayılan tip parametrelerini, TS 5.0 const type parameters ve TS 5.4 NoInfer ile çıkarımı yönetmeyi görüyoruz.

Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.

Önce Tahmin Et

İki değerden uzun olanı döndüren bir fonksiyon yazdık. Tip parametresinin yanındaki extends kısmına şimdilik takılma, koda bir bütün olarak bak:

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

const names = longest(["Ada"], ["Grace", "Alan"]);
const count = longest(10, 20);

İki soru: Hangi satır hata verir? Hata vermeyen satırda names'in type'ı nedir? Cevaplar, extends'in burada ne anlama geldiğini konuştuktan sonra.

T Her Şey Olabiliyorsa Hiçbir Şey Bilemezsin

Bir önceki yazının sonunda şu hatayla kalmıştık:

function getLength<T>(value: T): number {
  // Hata: Property 'length' does not exist on type 'T'.
  return value.length;
}

Kısıtsız bir T, fonksiyonun içinden bakınca kapalı bir kutu gibidir. İçinde string de olabilir, number da, null da. TypeScript kutunun içini bilmediği için onun üzerinde hiçbir property'ye izin vermez. Bu bir eksiklik değil, doğru davranış: fonksiyon her tiple çağrılabiliyorsa, gövdesi de her tiple çalışmak zorundadır.

Çözüm, kutuya bir etiket yapıştırmak: "Buraya sadece length'i olan şeyler girebilir." Bu etikete generic constraint, yani kısıt denir.

extends ile Kısıt Koymak

function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength("Ada"); // 3
getLength([1, 2, 3]); // 3
getLength({ length: 10, label: "custom" }); // 10
// Hata: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
getLength(42);

T extends { length: number } şunu söyler: "T hangi tip olursa olsun, en azından number tipinde bir length property'si taşımalı." Buradaki extends, class'lardaki inheritance ile karıştırılmasın. Generic bağlamında "atanabilir olmalı", yani "en az bu yapıya sahip olmalı" anlamına gelir. Daha önce gördüğümüz structural typing burada da geçerli: string, array ve length içeren herhangi bir object bu şartı sağlar, number ise sağlamaz.

Fonksiyonun içinde artık value.length kullanabiliyoruz, çünkü T ne olursa olsun bu property'nin var olduğu garanti. Kısıt iki yönlü çalışır: dışarıda çağıranı sınırlar, içeride sana bir garanti verir.

Şimdi baştaki soruya dönelim. longest(10, 20) satırı hata verir, çünkü number'ın length'i yoktur. names'in type'ı ise string[] olur. Asıl önemli nokta da burası. Aynı fonksiyonu generic olmadan, parametre tiplerini doğrudan { length: number } yaparak yazsaydık:

function longestPlain(a: { length: number }, b: { length: number }) {
  return a.length >= b.length ? a : b;
}

const names = longestPlain(["Ada"], ["Grace", "Alan"]); // { length: number; }
names.join(", "); // Hata: Property 'join' does not exist on type '{ length: number; }'.

Fonksiyon yine çalışırdı, ama dönüş tipi { length: number } olurdu ve elimizdeki değerin bir array olduğunu unuturduk. Generic sürümde kısıt sadece bir alt sınır; dönen şey yine T'nin kendisi. Kısıt "en az bunu bekliyorum" der, T ise "ve elimdeki tipin tamamını koruyorum" der.

Bir Tip Parametresini Diğerine Bağlamak

Kısıt sadece sabit bir yapı olmak zorunda değil; başka bir tip parametresine de bağlanabilir. En sık göreceğin örnek, bir object'ten key ile değer okuyan fonksiyondur:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Ada", age: 36 };

const userName = getProperty(user, "name"); // string
const userAge = getProperty(user, "age"); // number
// Hata: Argument of type '"email"' is not assignable to parameter of type '"age" | "name"'.
getProperty(user, "email");

keyof T, T'nin key'lerinden oluşan bir union üretir; burada "name" | "age". K extends keyof T da "K, T'nin key'lerinden biri olmalı" demektir. Böylece var olmayan bir key'i daha yazarken yakalarız. Hata mesajında union'ın "age" | "name" sırasıyla yazıldığına takılma; sıra anlam taşımaz. Dönüş tipindeki T[K] ise "T'nin K key'indeki property'nin tipi" demek; "name" için string, "age" için number geliyor. keyof'u ve bu köşeli parantezli tip yazımını ilerleyen yazılarda ayrıntılı ele alacağız; şimdilik iki tip parametresinin kısıtla birbirine bağlanabildiğini görmen yeterli.

Varsayılan Tip Parametreleri

Normal parametrelere varsayılan değer verebildiğin gibi tip parametrelerine de varsayılan tip verebilirsin:

function createList<T = string>(): T[] {
  return [];
}

const tags = createList(); // string[]
const scores = createList<number>(); // number[]

Bu fonksiyonun hiç parametresi yok, dolayısıyla TypeScript'in T'yi çıkarabileceği bir argüman da yok. Varsayılan olmasaydı createList() çağrısında T unknown olurdu ve unknown[] elde ederdik. Varsayılan tip, tam olarak çıkarım yapacak bir aday olmadığında devreye girer. Argümandan çıkarım yapılabiliyorsa çıkarım kazanır.

Bir kural var: varsayılanı olan tip parametreleri, varsayılanı olmayanlardan sonra gelmeli. <T = string, U> yazarsan Required type parameters may not follow optional type parameters. hatasını alırsın (TS2706). Varsayılan tip parametreleri asıl gücünü generic type'larda gösterir; bir sonraki yazıda orada tekrar karşılaşacağız.

const Type Parameters

Daha önce as const ile bir değerin literal ve readonly olarak çıkarılmasını sağlamıştık. Generic fonksiyonlarda da bazen çağıranın literal değerlerini korumak isteriz. TS 5.0 ile gelen const type parameter tam bunu yapar:

function defineRoutes<T extends readonly string[]>(paths: T): T {
  return paths;
}

function defineConstRoutes<const T extends readonly string[]>(paths: T): T {
  return paths;
}

const routes = defineRoutes(["/home", "/about"]); // string[]
const constRoutes = defineConstRoutes(["/home", "/about"]); // readonly ["/home", "/about"]

İlk fonksiyonda TypeScript array'i olağan şekilde string[]'e genişletiyor. İkincisinde tip parametresinin önündeki const, argümanı sanki as const yazılmış gibi çıkarmasını söylüyor ve sonuç readonly ["/home", "/about"] oluyor. Artık çağıran tarafın her seferinde as const yazmayı hatırlamasına gerek yok; bu sorumluluğu fonksiyon üstlendi.

Küçük ama önemli bir ayrıntı: kısıtı readonly string[] olarak yazdık. Kısıt string[] olsaydı TypeScript yine literal'leri korurdu, ama sonuç readonly olmayan ["/home", "/about"] tuple'ı olurdu. as const'taki gibi readonly bir sonuç istiyorsan kısıtı da readonly yaz; ayrıca readonly kısıt, fonksiyonun hem normal hem readonly array'leri kabul etmesini sağlar.

NoInfer: Çıkarıma Bu Parametreden Bakma

Bazen bir tip parametresi birden fazla yerde geçer ama sen T'nin sadece bunlardan birinden çıkarılmasını istersin. Şu örneğe bak:

function pickColor<C extends string>(colors: C[], fallback: C): C {
  return colors[0] ?? fallback;
}

const color = pickColor(["red", "green"], "blue"); // "blue" | "green" | "red"

Niyetimiz şuydu: fallback listedeki renklerden biri olmalı. Ama TypeScript C'yi çıkarırken iki parametreye birden baktı ve "blue"'yu da adaylara ekledi. Sonuç, hatayı yakalamak yerine genişleyen bir union oldu. TS 5.4 ile gelen NoInfer utility type'ı, bir parametreyi çıkarımın dışında tutar:

function pickColor<C extends string>(colors: C[], fallback: NoInfer<C>): C {
  return colors[0] ?? fallback;
}

const color = pickColor(["red", "green"], "red"); // "green" | "red"
// Hata: Argument of type '"blue"' is not assignable to parameter of type '"green" | "red"'.
pickColor(["red", "green"], "blue");

Artık C sadece colors'tan çıkarılıyor, fallback ise yalnızca denetleniyor. Zihinde şöyle tutabilirsin: NoInfer o parametreye "sen karar verme, sadece uy" der. Tip olarak bir şey değiştirmez, sadece çıkarımın hangi kaynaktan yapılacağını belirler.

Sık Düşülen Tuzaklar

En sık görülen hata, kısıtı sağlayan bir değeri T yerine döndürmeye çalışmaktır:

function withDefaultId<T extends { id: number }>(item: T): T {
  // Hata: Type '{ id: number; }' is not assignable to type 'T'.
  return { id: 1 };
}

Mesajın devamı her şeyi anlatıyor: '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ id: number; }'. Yani { id: 1 } kısıtı sağlıyor, ama T kısıtın kendisi değil. Biri bu fonksiyonu { id: 5, name: "Ada" } ile çağırırsa dönüş tipi name içeren bir object vaat eder; senin döndürdüğün object'te ise name yok. Kısıt bir alt sınırdır, T'nin tam karşılığı değildir.

İkinci tuzak, gerekmediği yerde generic'e kısıt eklemek. T sadece bir kez geçiyorsa, bir önceki yazıda gördüğümüz gibi generic'e hiç gerek yoktur; parametre tipini doğrudan kısıtın kendisi yapmak yeterlidir. function logLength<T extends { length: number }>(value: T): void yerine function logLength(value: { length: number }): void aynı işi görür.

Üçüncüsü, extends kelimesini görünce class inheritance düşünmek. Generic'lerde extends bir class hiyerarşisi kurmaz, sadece "atanabilir olmalı" şartı koyar. T extends string bir string literal type'ı da, string'in kendisini de kabul eder.

Küçük Bir Deneme

★☆☆ Derlenir mi, tahmin et: longest("Ada", ["Grace"]). Hata veriyorsa mesajda T için hangi tipin çıkarıldığını oku ve nedenini düşün.

★★☆ createList<T = string>() fonksiyonuna bir parametre ekle: createList<T = string>(...items: T[]): T[]. createList() ve createList(1, 2) çağrılarının type'larını tahmin et. Varsayılan tip hangisinde devreye giriyor?

★★★ pickColor'ın NoInfer'lı sürümünü const type parameter ile birleştir: <const C extends string>. Sonra pickColor(["red", "green"], "green") çağrısının dönüş tipine bak ve const'un burada fark yaratıp yaratmadığını açıkla.

Şu ana kadar generic'leri hep fonksiyonlarda kullandık. Ama tip parametresi bir type alias'a, bir interface'e ya da bir class'a da verilebilir. Bir sonraki yazıda Box<T>, Result<T, E> ve generic bir Stack class'ı yazarak generic'leri veri yapılarına taşıyacağız.