TypeScript tipleri şekillerine göre karşılaştırır, bu yüzden string tipinin iki alias'ı birbirinin yerine kullanılabilir. Branded type yalnızca tip sisteminde var olan bir etiket ekler, string & { readonly __brand: "UserId" }. Bu etiket bir UserId değerini düz bir string ile ve diğer her brand ile uyumsuz yapar:
Çalışma zamanında userId sadece "u_42" string'idir. Brand derleme zamanına ait bir etikettir ve tek görevi ID'lerin, birimlerin ve doğrulanmış string'lerin birbirine karışmasını önlemektir.
Sorun: Alias'lar Yalnızca Addır
Bir type alias yeni bir tip oluşturmaz. Var olan bir tipe ikinci bir ad verir ve derleyici iki adı da aynı şey olarak görür:
Buna yapısal tipleme denir: TypeScript şeklin uyup uymadığını kontrol eder ve string, string tipine uyar. Nesnelerde şekiller genellikle farklıdır; ID'lerde, e-postalarda, para birimlerinde ve birimlerde ise hiçbir zaman farklı değildir. Brand'ler bu tek durumu çözer.
Brand Nasıl Çalışır
string & { readonly __brand: "UserId" } bir intersection'dır: değer hem bir string olmalı hem de "UserId" tipinde bir __brand özelliğine sahip olmalıdır. Gerçek hiçbir string'in bu özelliği yoktur, bu yüzden hiçbir düz string bu tipe atanamaz:
index.ts(10,10): error TS2345: Argument of type 'string' is not assignable to parameter of type 'UserId'.
Type 'string' is not assignable to type '{ readonly __brand: "UserId"; }'.
index.ts(11,10): error TS2345: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'.
Type 'OrderId' is not assignable to type '{ readonly __brand: "UserId"; }'.
Types of property '__brand' are incompatible.
Type '"OrderId"' is not assignable to type '"UserId"'.
Önemli olan yön yine çalışır: bir UserId bir string'dir, bu yüzden onu string alan her şeye geçirebilir, üzerinde .startsWith() çağırabilir ya da bir template içine koyabilirsiniz. Brand yalnızca giriş yolunu kapatır.
Doğrulama Yapan Constructor Fonksiyonları
as UserId assertion'ı tek giriş yoludur ve bir assertion hiçbir şeyi kontrol etmez. Onu girdiyi doğrulayan tek bir fonksiyona koyun; böylece programdaki her branded değerin bu kontrolden geçtiği bilinir:
sendWelcome girdisini bir daha kontrol etmez, çünkü parametre tipi kontrolün zaten yapıldığını söyler. Bu "parse, don't validate" fikridir: sınırda kontrol edin, sonra kanıtı tipte taşıyın. Exception yerine boolean tercih ediyorsanız bir type guard da constructor olarak çalışır: function isEmail(s: string): s is Email.
Generic Bir Brand Yardımcısı
Her tip için intersection'ı elle yazmak tekrara dönüşür. Küçük bir generic bunu bir kez yapar:
Bir number'a brand vermek, bir string'e brand vermekle aynı şekilde çalışır. amount / 100 ifadesinin düz bir number olduğuna dikkat edin: branded bir number üzerindeki aritmetik brand'siz bir sonuç verir, bu konu aşağıda ele alınıyor.
unique symbol Brand'leri
__brand gibi string bir özellik adı gerçek bir özellik gibi görünür: userId.__brand tip kontrolünde "UserId" olarak geçer ama çalışma zamanında undefined olur ve iki kütüphane aynı adı seçebilir. Bir unique symbol anahtarı ikisini de önler:
declare const brand: unique symbol, yalnızca tip denetleyicisi için var olan bir symbol bildirir; declare anahtar kelimesi onun için hiç JavaScript üretilmeyeceği anlamına gelir. Symbol kendi modülünden export edilmediği için diğer dosyalardaki kod brand özelliğini adlandıramaz bile; bu yüzden o modülün dışında bir Meters elde etmenin tek yolları export ettiğiniz fonksiyonlar ya da bir as Meters assertion'ıdır.
Brand'lerin Çalışma Zamanında Maliyeti Yoktur
Derlenen çıktıda brand'den hiçbir iz kalmaz. Meters örneği için, derleyicinin eklediği modül başlığının altında üretilen satırlar bunlardır: declare, iki type alias ve her as kaybolmuştur, son satırdaki bastırılmış çağrı ise yine çalışır.
function toMeters(feet) {
return (feet * 0.3048);
}
const height = 10;
const inMeters = toMeters(height);
console.log(inMeters.toFixed(3)); // 3.048
// @ts-expect-error: Meters is not Feet
toMeters(inMeters);
Branded bir değer düz primitive'in kendisidir: typeof "string" ya da "number" verir, JSON.stringify onu her zamanki gibi yazar ve karşılaştırmalar önceki gibi çalışır. Öte yandan constructor fonksiyonunuz kontrol etmedikçe çalışma zamanında hiçbir şey kontrol edilmez. JSON'dan, bir veritabanından ya da bir URL'den okunan veri string olarak gelir ve ancak o fonksiyondan geçirdiğinizde bir UserId olur.
Aritmetik ve Metotlar Brand'i Düşürür
Branded bir değer üzerindeki işlemler temel tipi döndürür, çünkü brand + ya da .slice() işlemlerinin ürettiği şeyin parçası değildir:
type Cents = number & { readonly __brand: "Cents" };
const a = 500 as Cents;
const b = 250 as Cents;
const sum = a + b; // number, not Cents
const total: Cents = a + b; // error TS2322: Type 'number' is not assignable to type 'Cents'
const fixed = (a + b) as Cents; // re-brand when the result is still valid
Genellikle istediğiniz de budur: cent cinsinden iki tutarı toplamak cent verir, ama cent'i cent ile çarpmak vermez ve hangi işlemlerin anlamı koruduğunu yalnızca siz bilirsiniz. Kodunuzun ihtiyaç duyduğu işlemler için addCents(a: Cents, b: Cents): Cents gibi küçük yardımcılar yazın.
Branded Types Ne Zaman Kullanılır
Aynı primitive tipteki iki değeri karıştırmanın gerçek bir risk olduğu ve derleyicinin başka türlü yardım edemediği yerlerde brand kullanın:
| Durum | Örnek brand'ler |
|---|---|
| Farklı tablolardan gelen ID'ler | UserId, OrderId, ProductId |
| Doğrulanmış string'ler | Email, Url, NonEmptyString, Slug |
| Birimler ve para birimleri | Meters, Feet, Cents, Usd, Eur |
| Temizlenmiş ya da escape edilmiş metin | SafeHtml, SqlIdentifier |
| Aralığı olan sayılar | Percentage, PositiveInt |
Hiçbir zaman karıştırılmayan değerlerde ve şekli zaten farklı olan nesne tiplerinde bunları kullanmayın. Doğrulama kütüphaneleri bir şemadan branded tipler üretebilir: Zod'da z.string().brand<"UserId">(), parse metodu branded bir UserId döndüren bir şema verir; bu da constructor fonksiyonlarını elle yazma zahmetinden kurtarır.
Sıkça Sorulan Sorular
TypeScript'te branded type nedir?
Çalışma zamanında aynı temsile sahip iki tipi birbiriyle uyumsuz yapan bir desendir. Temel tipi, hiçbir sıradan değerin sahip olmadığı bir etiketle kesiştirirsiniz: type UserId = string & { readonly __brand: "UserId" }. Ardından düz bir string ya da farklı etiketli bir OrderId, UserId beklenen yerde reddedilir.
TypeScript'te nominal tipler var mı?
Hayır. TypeScript'in tip sistemi yapısaldır: aynı şekle sahip iki tip, adları ne olursa olsun birbirinin yerine kullanılabilir. private ya da #private üyeleri olan class bildirimleri nominal davranır; branded types ise string ve number gibi primitive'ler için aynı etkiyi elde etmenin yaygın yoludur.
Branded types'ın çalışma zamanı maliyeti var mı?
Hayır. Brand yalnızca tipte vardır. Değer çalışma zamanında yine düz bir string ya da number'dır, fazladan bir özelliği yoktur ve derlenen JavaScript brand olmadan yazılmış hâliyle aynıdır. Tek çalışma zamanı kodu, branded değerleri oluşturan fonksiyona koymayı seçtiğiniz doğrulamadır.
Branded bir tipten nasıl değer oluştururum?
Bir tip assertion'ı ile, tercihen girdiyi önce kontrol eden küçük bir fonksiyonun içinde: function toEmail(s: string): Email { if (!s.includes("@")) throw new Error("bad email"); return s as Email; }. as ifadesini bu tek yerde tutmak, programdaki her Email değerinin kontrolden geçtiği anlamına gelir.
Type alias ile branded type arasındaki fark nedir?
type UserId = string yalnızca yeni bir addır: UserId beklenen her yerde herhangi bir string kabul edilir. type UserId = string & { readonly __brand: "UserId" } ise yeni ve uyumsuz bir tiptir: düz bir string önce bir constructor fonksiyonundan ya da bir assertion'dan geçmelidir.