Anasayfa » 9. TypeScript’de Generic Tipler

9. TypeScript’de Generic Tipler

by Levent KARAGÖL
10 dakikalık okuma süresi

Bu makalemizde, TypeScript’de Generic tiplerin kullanım amaçlarını ve tanımını, birden fazla generic tipin bir arada kullanım şeklini, generic tiplerin ihtiyacımıza uygun şekilde kısıtlanmasını ve generic tiplerin sınıf seviyesinde kullanımını işleyeceğiz.


Generic Tiplere Neden İhtiyacımız Var?

Generic tipler, yazdığımız kodu kopyalamak ve revize edip tekrar yazmak yerine, benzer amaçlar için tekrar kullanabileceğimiz hale getirme amacıyla kullanılır.

TypeScript serisinde şimdiye kadar gördüğümüz konular nesnelere, fonksiyonlara veya sınıflara geçilen tiplere belirli kurallar ve kısıtlar koymak ile ilgiliydi. Örneğin şu fonksiyona şu kadar parametre geçilir, tipleri bu olmalıdır vs. şeklinde kurallar koyduk.

Ancak bazı durumlarda, geçilmesi gerekli parametrenin tipini, çağıran tarafın belirlemesi gerekebilir. Örneğin “number” tipinde parametre alan bir fonksiyonun bir de “string” tipinde parametre alan bir varyasyonuna da ihtiyaç duyabiliriz. Belki de daha sonra özelleştirilmiş bir tip alan varyasyonuna ihtiyaç duyarız. Bu durumda her yeni ihtiyaçta mevcut fonksiyonu kopyalamak, aldığı tipi değiştirmek ve revize etmek zorunda kalırız.

Bu tarz durumlarda generic tipler vasıtasıyla geçilecek parametrenin veya bazen de dönülecek tipin ne olduğu kararını, çağrım yapan tarafa bırakarak aynı kodun kopyalanmadan tekrar tekrar kullanılmasına imkân sağlayabiliriz.


Generic Fonksiyon Tanımı

Gelelim bu anlatılanları nasıl yapacağımıza. Diyelim ki bize öyle bir fonksiyon lazım ki, parametre olarak geçtiğimiz ilk sayısal değeri alıp bundan sayısal bir dizi oluşturarak bize geri dönsün. Gerekli kod aşağıdaki gibi olacaktır.

// Fonksiyonumuz number tipinde bir parametre alır ve number tipinde bir dizi geriye döner
function generateNumberArray(firstItem: number): number[] {
    return new Array<number>().concat(firstItem);
}

// İlk elemanı 1 olan bir dizi oluşturuyoruz
let numericArray = generateNumberArray(1);


// Tanıma uygun şekilde dizimizi kullanıyoruz
numericArray.push(2);

// Tanıma uymayan bir tip atanmaya çalışıldığında derleme sırasında hata alıyoruz
numericArray.push("Üç");

Buraya kadar her şey yolunda. Ancak ihtiyacımız değişti, artık aynı işi “string” veri alıp “string” dizi oluşturan bir versiyonuna da ihtiyacımız var. Kodumuzu uygun şekilde değiştirelim.

// Fonksiyonumuz number tipinde bir parametre alır ve number tipinde bir dizi geriye döner
function generateNumberArray(firstItem: number): number[] {
    return new Array<number>().concat(firstItem);
}

// Fonksiyonumuz string tipinde bir parametre alır ve string tipinde bir dizi geriye döner
function generateStringArray(firstItem: string): string[] {
    return new Array<string>().concat(firstItem);
}


// İlk elemanı 1 olan bir dizi oluşturuyoruz
let numericArray = generateNumberArray(1);

// İlk elemanı "Bir" olan bir dizi oluşturuyoruz
let stringArray = generateStringArray("Bir");


// Tanıma uygun şekilde dizilerimizi kullanıyoruz
numericArray.push(2);
stringArray.push("İki");

// Tanıma uymayan bir tip atanmaya çalışıldığında derleme sırasında hata alıyoruz
numericArray.push("Üç");
stringArray.push(3);

Tamam, belki amaç hasıl oldu. Ama bu işin sonu nereye varacak, meçhul. Yarın başka tiplere de ihtiyacımız olabilir. Hele ki özel tip tanımlarına girersek, bu fonksiyonlardan belki de onlarca yazmamız gerekecek. Bunun daha kolay bir yolu olmalı. Neden “any” kabul eden bir fonksiyon yazmıyoruz? Hadi deneyelim.

// Fonksiyonumuz any tipinde bir parametre alır ve any tipinde bir dizi geriye döner
function generateArray(firstItem: any): any[] {
    return new Array<any>().concat(firstItem);
}

// İlk elemanı 1 olan bir dizi oluşturuyoruz
let numericArray = generateArray(1);

// İlk elemanı "Bir" olan bir dizi oluşturuyoruz
let stringArray = generateArray("Bir");


// Tanıma uygun şekilde dizilerimizi kullanıyoruz
numericArray.push(2);
stringArray.push("İki");

// Tanıma uymayan bir tip atanmaya çalıştığımızda hata almıyoruz, sonuçta dizimiz any tipinde
numericArray.push("Üç");
stringArray.push(3);

Hmm, evet tek bir fonksiyon ile işimizi çözdük ancak dönen dizi “any” tipinde olduğu için artık tip denetimi yok. Aslında burada TypeScript değil, JavaScript yazmaya başladık.

Tip güvenliğinden vazgeçmeden bunu yapmanın bir yolu olmalı! Evet, generic tipler ile bunu yapabiliriz. Örneğimizi aşağıdaki şekilde değiştirelim.

// Fonksiyonumuz T tipinde bir parametre alır ve T tipinde bir dizi geriye döner
// T tipinin ne olacağına çağıran taraf karar verir
function generateArray<T>(firstItem: T): T[] {
    return new Array<T>().concat(firstItem);
}

// T tipini number olarak belirtiyoruz ve bize number[] tipinde bir dizi dönüyor
let numericArray: number[] = generateArray<number>(1);

// T tipini string olarak belirtiyoruz ve bize string[] tipinde bir dizi dönüyor
let stringArray: string[] = generateArray<string>("Bir");

// Tiplere uygun şekilde dizilerimizi kullanıyoruz
numericArray.push(2);
stringArray.push("İki");

// any'den farklı olarak dizilere uyumsuz bir tip atanmaya çalışıldığında derleme sırasında hata alıyoruz 
numericArray.push("Üç");
stringArray.push(3);

Yukarıdaki örnekte de görüldüğü gibi, generic tip tanımlarında tipin ne olacağına çağıran taraf karar verir ve bu tipi fonksiyon adına bitişik “<>” işaretleri arasında belirtir. Burada biri “number“, diğeri “string” iki farklı tip belirterek, iki farklı tipte dizi elde ettik. Hem tip güvenliğinden vazgeçmedik hem de fonksiyonları tekrar yazmak zorunda kalmadık.


Birden Fazla Generic Tip Kullanımı

Bazı durumlarda tek bir tipe karar vermek yeterli gelmeyebilir ve çağrım yapan tarafın birden fazla tip geçmesini isteyebiliriz. Böyle durumlarda “<>” işaretleri arasında virgül ile ayırmak suretiyle birden fazla tip geçmemiz de mümkündür. Konuyu bir örnek üzerinde inceleyelim.

// Fonksiyonumuz T1 ve T2 tipinde birer parametre alır ve bunlara uygun bir nesne yapısı döner
function generateKeyValue<T1, T2>(key: T1, value: T2): { key: T1, value: T2 } {
    return {key: key, value: value};
}

// T1 tipini string, T2 tipini number olarak belirtiyoruz
let keyValue1 = generateKeyValue<string, number>("Key1", 1);

// T1 tipini string, T2 tipini string olarak belirtiyoruz
let keyValue2 = generateKeyValue<string, string>("Key2", "Bir");

// T1 tipini string, T2 tipini Date olarak belirtiyoruz
let keyValue3 = generateKeyValue<string, Date>("Key3", new Date());

Görüldüğü gibi bu örneğimizde iki generic tip alan bir fonksiyon yazdık ve verilen tiplere uygun şekilde key/value değerlerini içeren bir nesne dönen fonksiyon elde ettik. Ardından aynı fonksiyonu üç farklı kombinasyonda tekrar tekrar kullandık.


Generic Tiplerin Kısıtlanması

Generic tipler bize ciddi bir esneklik sağlar ancak bazı durumlarda bu esnekliğe sınırlar koymak isteriz. Örneğin fonksiyonumuzda geçilen T tipinin “length” özelliğini kontrol etmemiz gerektiğini ve bu değerin 5’den büyük olma durumuna göre “boolean” bir değer dönmemiz gerektiğini düşünelim. Kodumuz aşağıdakine benzer bir yapıda olacaktır.

function checkLength<T>(value: T): boolean {
    return value.length > 5;
}

Ancak TypeScript böyle bir kod yazmamıza izin vermez. Çünkü T tipinde “length” isminde bir özellik olup olmadığını bilemeyiz. Biz belki yazdığımız kod itibariyle bu fonksiyona geçilecek tüm parametrelerin “length” özelliğine sahip olduğuna eminizdir ancak TypeScript kesinlik olmadığı için derleme sırasında hata verir.

Bu durumda fonksiyona geçilecek parametreler için bir kısıtlama koymamız gerekir. Kısıtlama demek, Interface demek. Kodumuzu aşağıdaki şekilde güncelleyelim.

interface ILength {
    length: number;
}

function checkLength<T extends ILength>(value: T): boolean {
    return value.length > 5;
}

const result: boolean = checkLength<string>("test");

Burada “ILength” adında bir Interface tanımladık ve tek şartımız, bu Interface’e uyacak tiplerin “length” adında “number” tipinde bir özelliğe sahip olmasıydı. Ardından fonksiyondaki generic tip tanımında “extends” ifadesi ile geçilen tüm tip tanımlarına bu şarta uyma kısıtı getirdik. Son olarak metinsel bir tip geçtik ki “string” tipin “length” özelliği olduğu için derleyici buna müsaade etti.


Generic Sınıf Tanımı

Fonksiyonlar gibi sınıflarda da generic tipler kullanılabilir. Bu yöntem genelde sınıf içerisindeki tüm fonksiyonlarda geçerli olacak bir tip tanımlaması sağlamak için kullanılır. Örneğin bir sınıf tasarlayalım ki, “add” ve “get” isminde iki fonksiyonu olsun. “add” fonksiyonu ile belirtilen tipteki değerleri local bir diziye kaydetsin ve “get” fonksiyonu ile de belirtilen pozisyondaki elemanı bize dönsün. Bu durumda her bir fonksiyon için ayrı ayrı generic tip geçemeyiz çünkü bu iki tipin birbiri ile aynı olması gerekir. Bu durumda generic tipi sınıf seviyesinde tanımlarız ve sınıfın örneğini çıkarttığımız sırada tipi belirtiriz. Bu durumda yazmamız gereken kod aşağıdaki gibi olacaktır.

class TypedList<T> {
    private values: T[] = [];

    public add(value: T): void {
        this.values.push(value);
    }

    public get(position: number): T {
        return this.values[position];
    }
}

const typeList = new TypedList<string>();

typeList.add("Bir");
typeList.add("İki");
typeList.add("Üç");

const result = typeList.get(1); // İki

Böylece TypeScript’de generic tipler konusunun sonuna geldik. Bir sonraki makalemizde TypeScript’de modüller konusunu işleyeceğiz.

Herkese iyi çalışmalar…

İLGİNİZİ ÇEKEBİLİR

Bir Yorum Yaz