İçeriğe geç
Muhammet Şafak
Diller 4 dk okuma

TypeScript'te generic'ler ve tip çıkarımı

TypeScript'te generic'leri kullanarak yeniden kullanılabilir, tip-güvenli soyutlamalar kurmak ve tip çıkarımından yararlanmak.


TypeScript’e geçişin ilk döneminde generic’lerden uzak durmaya çalıştım. Karmaşık görünüyordu; Array<T>, Promise<T> gibi built-in kullanımların ötesine geçmek gerekmeyecekmiş gibi düşündüm. Daha büyük bir kod tabanında çalışmaya başlayınca bu düşünce hızla değişti. Generic’leri anlamak, “tip eklenmiş JavaScript” olmaktan “gerçek tip güvenliği” olan bir koda geçişi sağlıyor.

Generic nedir

Generic, bir fonksiyon veya sınıfın kullandığı tip bilgisini çağrı anına kadar ertelemesini sağlar. any gibi tip güvenliğini terk etmeden, farklı tipler için aynı mantığı tekrar yazmadan reusable yapılar kurmanın yolu.

En basit örnekle başlayalım:

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

const num = identity(42);        // T => number olarak çıkarılır
const str = identity("merhaba"); // T => string olarak çıkarılır

T burada bir tür yer tutucu (placeholder). Fonksiyon çağrıldığında TypeScript hangi tip geçirildiğini anlayarak T’yi somutlaştırıyor.

Type inference ve ne zaman işe yarıyor

TypeScript’in güçlü yanlarından biri, generic parametresini çoğu zaman açıkça belirtmenize gerek kalmaması. Type inference derleyicinin bunu sizin yerinize çözmesi.

// Açık tip parametresi:
const arr = identity<number[]>([1, 2, 3]);

// Çıkarımla — derleyici zaten anlıyor:
const arr = identity([1, 2, 3]);

Bu, generic’leri kullanan kodu gereğinden ayrıntılı olmaktan kurtarıyor. Kütüphane yazan siz, kullanan da siz olduğunuzda bu ayrım daha net görünüyor.

Çıkarımın sınırları da var. Derleyici bağlamdan yeterli bilgi toplayamazsa — örneğin fonksiyon parametresiz çağrıldığında ya da dönüş tipi başka bir generic ile ilişkiliyse — tipi siz yazmalısınız. Bu durumu görmezden gelmek unknown ya da istemediğiniz bir genişlemeye yol açabilir.

Kısıtlamalar (constraints) ile generic’i sınırlamak

T’nin herhangi bir tip olabileceği bazen fazla geniş. extends anahtar sözcüğü ile generic’in hangi tipe sahip olması gerektiğini belirtebilirsiniz:

interface HasId {
    id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
    return items.find(item => item.id === id);
}

const users = [
    { id: 1, name: "Ahmet" },
    { id: 2, name: "Mehmet" },
];

const user = findById(users, 1); // T => { id: number; name: string }

T extends HasId diyerek “bu generic yalnızca id alanı olan tipler için çalışır” demiş olduk. TypeScript, item.id’ye erişimin güvenli olduğunu biliyor.

Bu kısıt aynı zamanda hata mesajlarını da anlamlı kılıyor. id alanı olmayan bir nesneyi geçirmeye çalıştığınızda derleyici size tam olarak hangi gereksinimin karşılanmadığını söylüyor. any kullansaydınız bu hatayı ancak çalışma zamanında — ve muhtemelen çok daha geç bir anda — görürdünüz.

Gerçek hayattan kullanım: API yanıtı sarmalayıcı

Projelerde sıkça kullandığım bir örnek: API yanıtlarını sarmak için tek bir generic tip.

interface ApiResponse<T> {
    data: T;
    success: boolean;
    message: string | null;
}

async function fetchUser(id: number): Promise<ApiResponse<User>> {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
}

async function fetchOrders(userId: number): Promise<ApiResponse<Order[]>> {
    const response = await fetch(`/api/users/${userId}/orders`);
    return response.json();
}

ApiResponse<T> bir kez tanımlandı; her endpoint için T yerine farklı tipler geliyor. Ortak yapıyı her yerde tekrar yazmak yerine tek bir generic tip kullanıyorsunuz.

Bunu pratikte test ettiğimde fark ettiğim bir şey: ApiResponse<T> sözleşmesi, istemci-sunucu entegrasyonunda hata bulmayı kolaylaştırıyor. Sunucu bir alan ekleyip yanıt tipini değiştirdiğinde, generic yapı sayesinde tüketen her yerde derleme hatası alıyorsunuz. Sessizce çalışan ancak hatalı veri işleyen kod yerine, derleyicinin sizi uyardığı bir yapı bu.

Birden fazla generic parametre

Bazen birden fazla tipe ihtiyaç duyulur:

function mapObject<K extends string, V, R>(
    obj: Record<K, V>,
    transform: (value: V, key: K) => R
): Record<K, R> {
    const result = {} as Record<K, R>;
    for (const key in obj) {
        result[key] = transform(obj[key], key);
    }
    return result;
}

const prices = { elma: 5, armut: 8, kiraz: 15 };
const discounted = mapObject(prices, (price) => price * 0.9);
// discounted: Record<string, number>

Birden fazla generic parametre kullanırken dikkat ettiğim bir nokta: parametrelerin birbirleriyle ilişkisini kısıtlamalar aracılığıyla netleştirmek. K extends string burada K’nın bir nesne anahtarı olarak geçerli olmasını garanti ediyor. Bu yazılmadığında TypeScript daha geniş bir tip çıkarır ve bazı hataları kaçırabilir.

Ne zaman generic, ne zaman union tip

Generic her yerde doğru araç değil. string | number gibi union tipler belirli, bilinen bir alternatifleri listesi için daha uygun. Generic ise “hangi tipin geleceğini bilmiyorum ama gelen tip ne olursa olsun tutarlılık istiyorum” durumu için.

Kural olarak şöyle düşünüyorum: kullanıcı hangi tipi geçireceğini belirleyecekse generic; kütüphane hangi tipleri kabul ettiğini belirleyecekse union.

Bir de aşırı generic kullanımına dikkat etmek gerekiyor. Sırf “esnek olsun” diye her fonksiyona generic eklemek, kodu okuyanın tip akışını takip etmesini zorlaştırıyor. Generic bir değer katmıyorsa — yani type inference yapılmıyor, kısıt konulmuyor, dönüş tipi giriş tipine bağlı değilse — büyük ihtimalle sade bir parametre tipi daha okunur.

Generic’leri anladıkça TypeScript’in tip sistemi bir kısıt olmaktan çıkıp gerçek bir araç haline geliyor. Özellikle yardımcı fonksiyonlar, hook’lar ve API katmanı için düzgün tasarlanmış generic’ler uzun vadede refaktör maliyetini ciddi oranda düşürüyor.

Etiketler: #TypeScript
Paylaş:

İlgili Yazılar

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi