Türkçe

Yapısal bir tip sisteminde nominal tipleme elde etmek için güçlü bir teknik olan TypeScript branded tiplerini keşfedin. Tip güvenliğini ve kod netliğini nasıl artıracağınızı öğrenin.

TypeScript Branded Tipleri: Yapısal Bir Sistemde Nominal Tip Belirleme

TypeScript'in yapısal tip sistemi esneklik sunar ancak bazen beklenmedik davranışlara yol açabilir. Branded tipler, nominal tiplemeyi zorunlu kılmanın bir yolunu sunarak tip güvenliğini ve kod netliğini artırır. Bu makale, branded tipleri ayrıntılı olarak inceler, uygulanmaları için pratik örnekler ve en iyi uygulamaları sunar.

Yapısal ve Nominal Tiplemeyi Anlamak

Branded tiplere dalmadan önce, yapısal ve nominal tipleme arasındaki farkı netleştirelim.

Yapısal Tipleme (Duck Typing)

Yapısal bir tip sisteminde, iki tip aynı yapıya (yani, aynı tiplere sahip aynı özelliklere) sahiplerse uyumlu kabul edilir. TypeScript yapısal tipleme kullanır. Şu örneği ele alalım:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // TypeScript'te geçerli

console.log(vector.x); // Çıktı: 10

Point ve Vector ayrı tipler olarak bildirilmiş olsalar da, TypeScript aynı yapıya sahip oldukları için bir Point nesnesini bir Vector değişkenine atamaya izin verir. Bu kullanışlı olabilir, ancak aynı şekle sahip olan mantıksal olarak farklı tipleri ayırt etmeniz gerektiğinde hatalara da yol açabilir. Örneğin, tesadüfen ekran piksel koordinatlarıyla eşleşebilecek enlem/boylam koordinatlarını düşünün.

Nominal Tipleme

Nominal bir tip sisteminde, tipler yalnızca aynı isme sahiplerse uyumlu kabul edilir. İki tip aynı yapıya sahip olsa bile, farklı isimleri varsa ayrı olarak ele alınırlar. Java ve C# gibi diller nominal tipleme kullanır.

Branded Tiplere Duyulan İhtiyaç

TypeScript'in yapısal tiplemesi, bir değerin yapısından bağımsız olarak belirli bir tipe ait olduğundan emin olmanız gerektiğinde sorun yaratabilir. Örneğin, para birimlerini temsil etmeyi düşünün. USD ve EUR için farklı tipleriniz olabilir, ancak her ikisi de sayı olarak temsil edilebilir. Onları ayırt edecek bir mekanizma olmadan, yanlışlıkla yanlış para birimi üzerinde işlem yapabilirsiniz.

Branded tipler, yapısal olarak benzer ancak tip sistemi tarafından farklı olarak ele alınan ayrı tipler oluşturmanıza izin vererek bu sorunu çözer. Bu, tip güvenliğini artırır ve aksi takdirde gözden kaçabilecek hataları önler.

TypeScript'te Branded Tipleri Uygulamak

Branded tipler, kesişim tipleri (intersection types) ve benzersiz bir sembol veya dize değişmezi (string literal) kullanılarak uygulanır. Fikir, bir tipe, onu aynı yapıya sahip diğer tiplerden ayıran bir "marka" (brand) eklemektir.

Sembolleri Kullanma (Önerilen)

Markalama için sembol kullanmak genellikle tercih edilir çünkü sembollerin benzersiz olacağı garanti edilir.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Bir sonraki satırın yorumunu kaldırmak bir tip hatasına neden olacaktır
// const invalidOperation = addUSD(usd1, eur1);

Bu örnekte, USD ve EUR, number tipine dayalı markalı tiplerdir. unique symbol, bu tiplerin farklı olmasını sağlar. createUSD ve createEUR fonksiyonları bu tiplerde değerler oluşturmak için kullanılır ve addUSD fonksiyonu yalnızca USD değerlerini kabul eder. Bir EUR değerini bir USD değerine eklemeye çalışmak bir tip hatasıyla sonuçlanacaktır.

Dize Değişmezlerini (String Literals) Kullanma

Markalama için dize değişmezlerini de kullanabilirsiniz, ancak dize değişmezlerinin benzersiz olacağı garanti edilmediğinden bu yaklaşım sembol kullanmaktan daha az sağlamdır.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Bir sonraki satırın yorumunu kaldırmak bir tip hatasına neden olacaktır
// const invalidOperation = addUSD(usd1, eur1);

Bu örnek, bir öncekiyle aynı sonucu elde eder, ancak semboller yerine dize değişmezleri kullanır. Daha basit olsa da, markalama için kullanılan dize değişmezlerinin kod tabanınızda benzersiz olduğundan emin olmak önemlidir.

Pratik Örnekler ve Kullanım Durumları

Branded tipler, yapısal uyumluluğun ötesinde tip güvenliğini zorunlu kılmanız gereken çeşitli senaryolara uygulanabilir.

ID'ler

UserID, ProductID ve OrderID gibi farklı ID türlerine sahip bir sistem düşünün. Tüm bu ID'ler sayı veya dize olarak temsil edilebilir, ancak farklı ID türlerinin yanlışlıkla karıştırılmasını önlemek istersiniz.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... kullanıcı verilerini getir
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... ürün verilerini getir
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// Bir sonraki satırın yorumunu kaldırmak bir tip hatasına neden olacaktır
// const invalidCall = getUser(productID);

Bu örnek, branded tiplerin bir ProductID'nin UserID bekleyen bir fonksiyona geçirilmesini nasıl önleyebileceğini göstererek tip güvenliğini artırır.

Alana Özgü Değerler

Branded tipler, kısıtlamaları olan alana özgü değerleri temsil etmek için de yararlı olabilir. Örneğin, her zaman 0 ile 100 arasında olması gereken yüzdeler için bir tipiniz olabilir.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Yüzde 0 ile 100 arasında olmalıdır');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // Bir sonraki satırın yorumunu kaldırmak çalışma zamanında hataya neden olacaktır
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Bu örnek, çalışma zamanında (runtime) markalı bir tipin değeri üzerinde bir kısıtlamanın nasıl zorunlu kılınacağını gösterir. Tip sistemi bir Percentage değerinin her zaman 0 ile 100 arasında olduğunu garanti edemese de, createPercentage fonksiyonu bu kısıtlamayı çalışma zamanında zorunlu kılabilir. Markalı tiplerin çalışma zamanı doğrulaması için io-ts gibi kütüphaneleri de kullanabilirsiniz.

Tarih ve Saat Gösterimleri

Çeşitli formatlar ve saat dilimleri nedeniyle tarihler ve saatlerle çalışmak zor olabilir. Branded tipler, farklı tarih ve saat gösterimleri arasında ayrım yapmaya yardımcı olabilir.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Tarih dizesinin UTC formatında olduğunu doğrulayın (örn. Z ile ISO 8601)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Geçersiz UTC tarih formatı');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Tarih dizesinin yerel tarih formatında olduğunu doğrulayın (örn. YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Geçersiz yerel tarih formatı');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Saat dilimi dönüşümünü gerçekleştirin
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

Bu örnek, UTC ve yerel tarihler arasında ayrım yaparak uygulamanızın farklı bölümlerinde doğru tarih ve saat gösterimiyle çalıştığınızdan emin olmanızı sağlar. Çalışma zamanı doğrulaması, yalnızca doğru biçimlendirilmiş tarih dizelerinin bu tiplere atanabilmesini sağlar.

Branded Tipleri Kullanmak İçin En İyi Uygulamalar

TypeScript'te branded tipleri etkili bir şekilde kullanmak için aşağıdaki en iyi uygulamaları göz önünde bulundurun:

Branded Tiplerin Avantajları

Branded Tiplerin Dezavantajları

Branded Tiplere Alternatifler

Branded tipler TypeScript'te nominal tipleme elde etmek için güçlü bir teknik olsa da, düşünebileceğiniz alternatif yaklaşımlar da vardır.

Opak Tipler (Opaque Types)

Opak tipler, branded tiplere benzer ancak altta yatan tipi gizlemek için daha açık bir yol sunar. TypeScript'in opak tipler için yerleşik desteği yoktur, ancak modülleri ve özel sembolleri kullanarak bunları simüle edebilirsiniz.

Sınıflar (Classes)

Sınıfları kullanmak, ayrı tipleri tanımlamak için daha nesne yönelimli bir yaklaşım sağlayabilir. Sınıflar TypeScript'te yapısal olarak tiplenmiş olsa da, daha net bir görev ayrımı sunarlar ve metotlar aracılığıyla kısıtlamaları zorunlu kılmak için kullanılabilirler.

`io-ts` veya `zod` gibi Kütüphaneler

Bu kütüphaneler, gelişmiş çalışma zamanı tip doğrulaması sağlar ve hem derleme zamanı hem de çalışma zamanı güvenliğini sağlamak için branded tiplerle birleştirilebilir.

Sonuç

TypeScript branded tipleri, yapısal bir tip sisteminde tip güvenliğini ve kod netliğini artırmak için değerli bir araçtır. Bir tipe bir "marka" ekleyerek, nominal tiplemeyi zorunlu kılabilir ve yapısal olarak benzer ancak mantıksal olarak farklı tiplerin yanlışlıkla karıştırılmasını önleyebilirsiniz. Branded tipler bir miktar karmaşıklık ve ek yük getirse de, artan tip güvenliği ve kodun sürdürülebilirliğinin faydaları genellikle dezavantajlarından daha ağır basar. Bir değerin yapısından bağımsız olarak belirli bir tipe ait olduğundan emin olmanız gereken senaryolarda branded tipleri kullanmayı düşünün.

Yapısal ve nominal tiplemenin ardındaki ilkeleri anlayarak ve bu makalede özetlenen en iyi uygulamaları uygulayarak, daha sağlam ve sürdürülebilir TypeScript kodu yazmak için branded tiplerden etkili bir şekilde yararlanabilirsiniz. Para birimlerini ve ID'leri temsil etmekten alana özgü kısıtlamaları zorunlu kılmaya kadar, branded tipler projelerinizde tip güvenliğini artırmak için esnek ve güçlü bir mekanizma sağlar.

TypeScript ile çalışırken, tip doğrulaması ve zorlaması için mevcut olan çeşitli teknikleri ve kütüphaneleri keşfedin. Kapsamlı bir tip güvenliği yaklaşımı elde etmek için branded tipleri io-ts veya zod gibi çalışma zamanı doğrulama kütüphaneleriyle birlikte kullanmayı düşünün.

TypeScript Branded Tipleri: Yapısal Bir Sistemde Nominal Tip Belirleme | MLOG