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

TypeScript'te Generic Interface, Type ve Class'lar: Box, Result ve Stack

Mert Özen 3 Kas 2026 7 dk 0 görüntülenme
TypeScript'te Generic Interface, Type ve Class'lar: Box, Result ve Stack

Tip parametresini fonksiyonlardan tiplere taşıyoruz: generic type alias ile Box<T>, generic interface ile sayfalı liste, Result<T, E> ile başarı ve hata, generic Stack ve Registry class'ları.

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

Önce Tahmin Et

Bu kez tip parametresini bir fonksiyona değil, bir class'a veriyoruz. Basit bir stack yazdık:

class Stack<T> {
  #items: T[] = [];

  push(item: T): void {
    this.#items.push(item);
  }

  pop(): T | undefined {
    return this.#items.pop();
  }
}

const numbers = new Stack<number>();
numbers.push(1);
numbers.push("2");

const anything = new Stack();
anything.push(1);
anything.push("2");
const last = anything.pop();

Tahmin et: hangi push satırları hata verir? Son satırda last'in type'ı nedir? Cevap, generic class'ları konuştuğumuz bölümde.

Generic Type'larla Zaten Tanışıksın

İlk iki yazıda tip parametresini fonksiyonlara verdik. Aynı fikir tiplerin kendisine de uygulanabilir ve sen bunu farkında olmadan zaten kullanıyorsun. Daha önce gördüğümüz Array<string> yazımı, Array adlı generic bir interface'e string tip argümanı vermekten başka bir şey değil. Map<string, number>, Set<string>, ReadonlyArray<T> da öyle. Bu yazıda kendi generic type'larımızı yazacağız.

Zihinsel model şu: Array tek başına bir tip değil, tip üreten bir kalıp. Array<string> ise o kalıptan çıkmış gerçek bir tip. Generic fonksiyon bir değer üretirken tipi parametre olarak alıyordu; generic type ise doğrudan bir tip üretiyor.

Generic Type Alias: Box<T>

En basit örnekle başlayalım, içinde tek bir değer taşıyan bir kutu:

type Box<T> = { value: T };

const age: Box<number> = { value: 36 };
const label: Box<string> = { value: "Ada" };

// Hata: Generic type 'Box' requires 1 type argument(s).
const broken: Box = { value: true };

Type alias adından sonra gelen <T>, fonksiyonlardaki gibi bir tip parametresi tanımlar. Önemli bir fark var: generic fonksiyon çağırırken TypeScript T'yi argümandan çıkarıyordu, ama bir değişkene annotation olarak Box yazdığında çıkarım yapılmaz. Kalıbın hangi tiple doldurulacağını senin söylemen gerekir; söylemezsen requires 1 type argument(s) hatası gelir (TS2314).

Çıkarımın rahatlığını geri almak istersen generic type'ı generic bir fonksiyonla birleştirirsin:

type Box<T> = { value: T };

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

const boxedName = box("Ada"); // Box<string>
const boxedFlag = box(true); // Box<boolean>

Fonksiyon tarafında T çıkarılıyor, sonra Box<T>'ye aktarılıyor. Tip yazmadan, doğru tipte bir kutu elde ediyoruz.

Generic Interface'ler

Interface'ler de tip parametresi alabilir. API'lerden gelen sayfalı listeler bunun için iyi bir örnek: liste yapısı hep aynı, içindeki element'lerin tipi değişiyor.

interface ApiList<T> {
  items: T[];
  total: number;
  page: number;
}

interface Product {
  id: string;
  title: string;
  price: number;
}

const products: ApiList<Product> = {
  items: [{ id: "p1", title: "Mechanical Keyboard", price: 120 }],
  total: 1,
  page: 1,
};

const titles = products.items.map((product) => product.title); // string[]

ApiList<Product> yazdığımız anda items'ın Product[] olduğu biliniyor; map'teki product parametresinin tipi de buradan geliyor. Yarın kullanıcı listesi için ayrı bir interface yazmana gerek yok, ApiList<User> yeterli. Daha önce gördüğümüz type alias ve interface seçimi burada da aynı: object yapıları için ikisi de çalışır.

Result<T, E>: Başarıyı ve Hatayı Tipe Yazmak

Generic type'ların en kullanışlı örneklerinden biri, bir işlemin ya başarılı ya da başarısız olabileceğini ifade eden Result type'ı. Daha önce gördüğümüz discriminated union'ı iki tip parametresiyle birleştiriyoruz:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parseAge(input: string): Result<number, string> {
  const age = Number(input);
  if (Number.isNaN(age)) {
    return { ok: false, error: `Not a number: ${input}` };
  }
  return { ok: true, value: age };
}

const result = parseAge("36");

if (result.ok) {
  console.log(result.value + 1); // 37
} else {
  console.log(result.error.toUpperCase());
}

T başarılı sonucun tipini, E hatanın tipini taşıyor. ok property'si discriminant görevi görüyor: if (result.ok) bloğunun içinde TypeScript value'nun var olduğunu, else bloğunda ise error'ın var olduğunu biliyor. Kontrol etmeden result.value'ya erişmeye çalışırsan TypeScript seni durdurur, çünkü sonuç başarısız da olabilir.

E = Error kısmı geçen yazıda gördüğümüz varsayılan tip parametresi. Hata tipini özellikle belirtmek istemediğin yerde Result<User> yazman yeterli, E otomatik olarak Error olur. Varsayılan tip parametreleri asıl burada, generic type'larda işe yarar. Hata modellemenin ayrıntılarına hata yönetimi yazısında döneceğiz.

Generic Class'lar

Baştaki Stack<T>'e dönelim. Class adından sonra gelen <T>, class'ın tüm instance property'leri ve method'ları için geçerli olan bir tip parametresi tanımlar. new Stack<number>() dediğinde o instance'ın T'si number olur ve push artık sadece number kabul eder. Bu yüzden numbers.push("2") satırı Argument of type 'string' is not assignable to parameter of type 'number'. hatası verir.

Peki new Stack()? Burada ne tip argümanı verdik ne de TypeScript'in çıkarım yapabileceği bir constructor argümanı var. Böyle bir durumda T unknown olur. anything bir Stack<unknown>'tur, her şeyi kabul eder ve iki push da hatasız derlenir. Karşılığında last'in type'ı unknown olur; içinden ne çıktığını bilmeden onu kullanamazsın. Yani hata veren tek satır numbers.push("2").

Class'ın constructor'ı T tipinde bir parametre alırsa, generic fonksiyonlardaki çıkarım burada da çalışır:

class Stack<T> {
  #items: T[];

  constructor(initial: T[] = []) {
    this.#items = [...initial];
  }

  push(item: T): void {
    this.#items.push(item);
  }

  peek(): T | undefined {
    return this.#items.at(-1);
  }

  get size(): number {
    return this.#items.length;
  }
}

const words = new Stack(["Ada", "Grace"]); // Stack<string>
const scores = new Stack<number>(); // Stack<number>
words.push("Alan");
console.log(words.peek(), words.size); // Alan 3

new Stack(["Ada", "Grace"]) çağrısında T, constructor'a verilen array'den string olarak çıkarılıyor. Boş başlayan bir stack'te ise çıkarım yapacak bir şey olmadığı için tipi açıkça yazıyoruz. Kısaca: değer veriyorsan TypeScript bulur, vermiyorsan sen söylersin. Kullandığımız #items, JavaScript serilerinde gördüğümüz private field; TypeScript'in kendi private anahtar kelimesiyle farkını bir sonraki yazıda konuşacağız.

Birden Fazla Tip Parametresi

Bir yapı birden fazla tipe bağlıysa, tip parametrelerini virgülle ayırırsın. Result<T, E> bunun bir örneğiydi. Key'i belirli değerlerle sınırlı, value'su başka tipte olan küçük bir kayıt defteri yazalım:

class Registry<K extends string, V> {
  #entries = new Map<K, V>();

  set(key: K, value: V): void {
    this.#entries.set(key, value);
  }

  get(key: K): V | undefined {
    return this.#entries.get(key);
  }
}

const prices = new Registry<"basic" | "pro", number>();
prices.set("pro", 49);
console.log(prices.get("pro")); // 49
// Hata: Argument of type '"enterprise"' is not assignable to parameter of type '"basic" | "pro"'.
prices.set("enterprise", 99);

K'ye bir constraint verdik, V'yi serbest bıraktık; ikisi bağımsız çalışıyor. İçeride kullandığımız Map<K, V> da iki tip parametreli bir generic, ve bizim K ile V'miz ona aktarılıyor. Generic'ler böyle iç içe geçerek büyür.

İsimlendirme hakkında bir not: tek ya da iki parametrede T, K, V, E gibi tek harfler okunur kalır. Parametre sayısı arttığında TInput, TOutput gibi açıklayıcı adlar kodu çok daha anlaşılır yapar.

Sık Düşülen Tuzaklar

Birinci tuzak, tip argümanlarını yarım vermek. İki tip parametresi olan bir class'a tek argüman verirsen TypeScript eksik olanı çıkarmaya çalışmaz:

class Registry<K extends string, V> {
  #entries = new Map<K, V>();
}

// Hata: Expected 2 type arguments, but got 1.
const plans = new Registry<"basic">();

Kural: tip argümanlarının ya hepsini verirsin ya hiçbirini. Varsayılanı olan parametreler bu kuralın tek istisnasıdır; onları atlayabilirsin.

İkinci tuzak, static üyelerde T kullanmaya çalışmak:

class Settings<T> {
  // Hata: Static members cannot reference class type parameters.
  static defaults: T;
}

Bunun mantığı basit: static üyeler instance'a değil, class'ın kendisine aittir. Settings<string> ve Settings<number> instance'ları aynı class'ı paylaşır, yani tek bir defaults vardır. Hangi T'ye ait olacağı belli olmadığı için TypeScript buna izin vermez.

Üçüncü tuzak, baştaki örnekte gördüğümüz sessiz unknown. new Stack() ya da new Map() yazıp tip argümanını unuttuğunda hata almazsın, ama elindeki yapı unknown taşır ve sorun ancak içinden bir değer okumaya çalıştığında ortaya çıkar. Boş başlayan generic yapılarda tip argümanını baştan yaz.

Küçük Bir Deneme

★☆☆ Derlenir mi, tahmin et: const failed: Result<number> = { ok: false, error: "oops" };. Hata veriyorsa E'nin varsayılanını düşünerek nedenini açıkla.

★★☆ ApiList<T>'yi kullanarak { name: string; email: string } tipinde kullanıcılar taşıyan bir liste tanımla. Sonra items'a email property'si eksik bir kullanıcı eklemeyi dene ve hata mesajını oku.

★★★ Stack<T>'e generic bir method ekle: map<U>(fn: (item: T) => U): Stack<U>. Class'ın T'si ile method'un U'su birlikte nasıl çalışıyor? words.map((word) => word.length) çağrısının type'ının Stack<number> olduğunu doğrula.

Bu yazıda class'ları generic'lerin taşıyıcısı olarak kullandık, ama TypeScript'in class'lara kattığı şeyler bununla sınırlı değil. Bir sonraki yazıda public, private ve protected'ı, JavaScript'in #private'ıyla farklarını, readonly, abstract, implements ve override'ı göreceğiz.