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

TypeScript'te Generic'lere Giriş: Tipi de Parametre Olarak Almak

Mert Özen 3 Kas 2026 8 dk 0 görüntülenme
TypeScript'te Generic'lere Giriş: Tipi de Parametre Olarak Almak

Generic fonksiyonların neden var olduğunu, tip parametresinin girdi ile çıktıyı nasıl bağladığını, TypeScript'in T'yi nasıl çıkardığını ve generic'lerin any ile unknown'dan farkını görüyoruz.

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

Önce Tahmin Et

Temel seriyi okuduğunu, type inference'tan narrowing'e, fonksiyon tiplerinden tsconfig'e kadar günlük TypeScript'in elinde olduğunu varsayıyoruz. Orta seride bu temelin üstüne tekrar kullanılabilir tipler kuracağız: generic'ler, class'lar, utility type'lar, type guard'lar, module'ler ve declaration dosyaları. İlk durak generic'ler. Şu koda bak:

function echoAny(value: any): any {
  return value;
}

function echo<T>(value: T): T {
  return value;
}

const a = echoAny(42);
const b = echo(42);

console.log(a.toUpperCase());
console.log(b.toUpperCase());

İki fonksiyon da aldığı değeri olduğu gibi geri veriyor. Tahmin et: bu kod derlenir mi? Hata verirse hangi satırda? Derlenen satır çalıştığında ne olur? Cevabı, tip parametresinin ne yaptığını gördükten sonra vereceğiz.

Aynı İşi Yapan İki Fonksiyon

Bir array'in ilk element'ini döndüren bir fonksiyon yazmak istediğini düşün. Daha önce gördüğümüz fonksiyon tipleriyle ilk akla gelen şey şu olur:

function firstString(items: string[]): string | undefined {
  return items[0];
}

function firstNumber(items: number[]): number | undefined {
  return items[0];
}

const firstUser = firstString(["Ada", "Alan"]);
const firstScore = firstNumber([90, 75]);

İki fonksiyonun gövdesi harfi harfine aynı. Değişen tek şey tip. Yarın bir boolean[] ya da bir kullanıcı listesi için de aynı fonksiyon gerekecek ve her seferinde kopyala-yapıştır yapacaksın. JavaScript'te bu sorun yoktu, çünkü tip yoktu; tek bir fonksiyon her array'le çalışıyordu. Biz hem tek bir fonksiyon hem de tip güvenliği istiyoruz.

any ile Kestirme Yol ve Kaybolan Bilgi

Akla gelen ikinci çözüm, tipi any yapıp geçmek:

function firstItem(items: any[]): any {
  return items[0];
}

const user = firstItem(["Ada", "Alan"]);
user.toFixed(2); // Derlenir; çalışınca TypeError fırlatır

Fonksiyon artık her array'i kabul ediyor, ama bedeli ağır. İçeri string'lerden oluşan bir array girdi, dışarı any çıktı. TypeScript, user'ın bir string olduğunu unuttu. Bu yüzden user.toFixed(2) satırına itiraz etmiyor ve kod çalıştığında TypeError: user.toFixed is not a function hatasıyla duruyor. Daha önce gördüğümüz gibi any, o değer için tip denetimini kapatmak demektir. Sorunu çözmedik, sadece görünmez yaptık.

Asıl kaybettiğimiz şey girdi ile çıktı arasındaki ilişki. Söylemek istediğimiz cümle aslında çok basit: "Hangi tipte element'lerden oluşan bir array verirsen, o tipte bir element geri alırsın."

Tip Parametresi: Fonksiyona Tipi de Göndermek

Generic'ler tam olarak bu cümleyi yazmamızı sağlar:

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const firstUser = first(["Ada", "Alan"]); // string | undefined
const firstScore = first([90, 75]); // number | undefined

Fonksiyon adından hemen sonra gelen <T>, bir tip parametresi tanımlar. Normal bir parametre fonksiyona bir değer taşır; tip parametresi ise bir tip taşır. T, fonksiyon her çağrıldığında yerine gerçek bir tipin konacağı bir yer tutucudur. items: T[] "T tipinde element'lerden oluşan bir array" der, dönüş tipindeki T | undefined ise "aynı T'den bir değer ya da undefined" der. İki yer aynı harf üzerinden birbirine bağlandığı için ilişki kodun içinde yazılı hâle gelir.

Bunu bir kalıp gibi düşünebilirsin. Fonksiyonu bir kez yazıyorsun, ama içinde boş bir delik bırakıyorsun. Fonksiyonu string[] ile çağırdığında delik string ile, number[] ile çağırdığında number ile doluyor. Fonksiyonun gövdesi tek, ama her çağrının tipi kendine özel.

Adın T olması bir kural değil, bir gelenek ("Type"ın baş harfi). İkinci bir tip parametresi gerektiğinde genellikle U, key ve value için K ve V kullanılır. Anlamı daha açık olsun istersen TItem gibi uzun adlar da tamamen geçerlidir.

TypeScript T'yi Nasıl Buluyor?

Dikkat ettiysen first'ü çağırırken hiçbir yerde "T burada string olsun" demedik. TypeScript, T'yi gönderdiğin argümana bakarak kendisi çıkardı. Buna tip parametresi çıkarımı (type argument inference) denir ve generic fonksiyonların çoğunu bu sayede hiç tip yazmadan kullanırsın:

function echo<T>(value: T): T {
  return value;
}

const a = echo("Ada"); // "Ada"
let b = echo("Ada"); // string
const c = echo<string>("Ada"); // string
const d = echo({ name: "Ada", age: 36 }); // { name: string; age: number; }

İlk iki satırın farkı tanıdık gelmeli. Daha önce let ve const arasındaki widening farkını görmüştük; burada da aynı kural işliyor. const ile T literal type olan "Ada" olarak kalıyor, let ile string'e genişliyor. Üçüncü satırda ise T'yi açıkça kendimiz verdik. Fonksiyon adından sonra köşeli parantez içinde yazılan bu tipe explicit type argument denir.

Çıkarım her zaman senin aklındaki sonucu vermez. TypeScript T için birden fazla aday gördüğünde ilk argümandaki adayı tercih eder:

function pair<T>(x: T, y: T): [T, T] {
  return [x, y];
}

const p1 = pair(1, 2); // [number, number]
// Hata: Argument of type 'string' is not assignable to parameter of type 'number'.
const p2 = pair(1, "two");
const p3 = pair<number | string>(1, "two"); // [string | number, string | number]

pair(1, "two") çağrısında T, ilk argümandan number olarak belirlenir ve ikinci argüman bu kurala uymadığı için hata alırsın. Gerçekten bir karışım istiyorsan T'yi açıkça number | string olarak verirsin. Pratik kural: önce çıkarıma güven, explicit type argument'i ancak çıkarım yanlış ya da yetersiz kaldığında yaz.

Generic'ler any'den Ne Farkla Ayrılır?

Artık baştaki soruya dönebiliriz. echoAny(42) çağrısının sonucu any olduğu için a.toUpperCase() satırı derlenir; çalıştığında ise TypeError: a.toUpperCase is not a function fırlatır. echo(42) çağrısında ise T, 42 literal type'ı olarak çıkarılır ve b'nin type'ı 42 olur. TypeScript ikinci satırı daha çalıştırmadan reddeder:

error TS2339: Property 'toUpperCase' does not exist on type '42'.

Farkı tek cümlede söylersek: any tip bilgisini atar, generic ise tip bilgisini taşır. İkisi de "her tipi kabul ederim" der, ama generic buna "ve sana aynı tipi geri veririm" diye ekler.

Peki unknown? Daha önce onu güvenli any olarak tanımıştık. Burada da güvenlidir, ama aynı bilgi kaybını yaşatır:

function echoUnknown(value: unknown): unknown {
  return value;
}

const u = echoUnknown(42);
// Hata: 'u' is of type 'unknown'.
u.toFixed(2);

Değer 42 olduğu hâlde toFixed çağıramıyorsun, çünkü dönüş tipi sadece "bir şey" diyor. Her kullanımda narrowing yapman gerekir. Generic sürüm ise bu bilgiyi kendiliğinden koruyor. Kısaca: any güvensiz, unknown güvenli ama unutkan, generic hem güvenli hem hafızalı.

Arrow Fonksiyonlar ve Generic Fonksiyon Tipleri

Tip parametresi arrow fonksiyonlarda parametre listesinin önüne yazılır. Generic bir fonksiyonun tipini bir type alias olarak da tanımlayabilirsin:

const last = <T>(items: T[]): T | undefined => items[items.length - 1];

type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];

const mapAll: Mapper = (items, fn) => items.map(fn);

const lengths = mapAll(["Ada", "Grace"], (name) => name.length); // number[]

Mapper, iki tip parametresi alan bir function type expression. mapAll(["Ada", "Grace"], …) çağrısında TypeScript önce T'yi array'den string olarak çıkarıyor, sonra callback'in name parametresine bu tipi veriyor. Callback name.length döndürdüğü için R de number oluyor. Hiçbir yere tip yazmadan number[] elde ettik. JavaScript serilerinde kullandığın map, filter, find gibi array method'larının tipleri de tam olarak böyle generic olarak tanımlanmıştır; editörde üzerlerine geldiğinde imzalarındaki <U>'yu görebilirsin.

Sık Düşülen Tuzaklar

Birinci tuzak: T'nin içinde ne olduğunu bilmeden onu kullanmaya çalışmak.

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

Bu hata ilk başta şaşırtır, ama mantıklıdır. T her tip olabilir; number da olabilir, boolean da. Bunların length'i yoktur, bu yüzden TypeScript hiçbir varsayım yapmaz. T'nin belirli bir yapıya sahip olmasını şart koşmanın yolu var; bir sonraki yazının konusu tam olarak bu.

İkinci tuzak: tip parametresini tek bir yerde kullanmak. Generic'in değeri iki yeri birbirine bağlamasından gelir. T sadece bir parametrede geçiyor ve hiçbir yerle ilişki kurmuyorsa generic'e gerek yoktur:

// Gereksiz: T yalnızca bir kez geçiyor
function printValue<T>(value: T): void {
  console.log(value);
}

// Aynı işi görür
function printAnything(value: unknown): void {
  console.log(value);
}

Üçüncü tuzak daha sinsi: T'yi sadece dönüş tipinde kullanmak.

function parseJson<T>(text: string): T {
  return JSON.parse(text);
}

const user = parseJson<{ name: string }>('{"title":"oops"}');
console.log(user.name.toUpperCase()); // Derlenir; çalışınca TypeError fırlatır

Bu fonksiyon generic görünüyor, ama aslında kılık değiştirmiş bir type assertion. JSON.parse any döndürdüğü için TypeScript ne dersen ona inanır. Gelen veride name olmadığı için user.name undefined çıkar ve kod TypeError: Cannot read properties of undefined (reading 'toUpperCase') ile durur. Tipler çalışma zamanında silindiği için dışarıdan gelen veriyi gerçekten doğrulamak gerekir; bunu asenkron kodda tipler konuşurken ayrıntılı göreceğiz.

Küçük Bir Deneme

★☆☆ function wrapInArray<T>(value: T): T[] { return [value]; } fonksiyonunu yaz. const w = wrapInArray("Ada"); satırında w'nin type'ını tahmin et, sonra editörde üzerine gelerek kontrol et. echo("Ada")'daki gibi literal type'ı koruyup "Ada"[] mi oluyor, yoksa string[] mi?

★★☆ Derlenir mi, tahmin et: pair("a", "b"), pair("a", 1) ve pair<string | number>("a", 1). Hata veren çağrıda TypeScript T'yi neye göre belirledi?

★★★ İki element'li bir tuple'ın element'lerinin yerini değiştiren swap fonksiyonunu iki tip parametresiyle yaz: [A, B] alsın, [B, A] döndürsün. swap(["Ada", 36]) çağrısının type'ının [number, string] olduğunu doğrula.

Bu yazıda T'ye hiçbir şart koymadık; her tipi kabul etti ve biz de onun hakkında hiçbir şey bilemedik. Bir sonraki yazıda T'ye sınır çizeceğiz: extends ile "sadece length'i olan tipler" demeyi, varsayılan tip parametrelerini ve çıkarımı yönlendiren const ile NoInfer'ı göreceğiz.