TypeScript'te Type Alias ve Interface: Tiplere İsim Vermek
type alias ve interface ile tiplere isim vermeyi, extends ile intersection arasındaki farkı, declaration merging'i, hangisinin ne zaman seçileceğini ve isimlendirme alışkanlıklarını örneklerle görüyoruz.
Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.
Önce Tahmin Et
Bir önceki yazıda object tiplerini satır içinde yazmayı gördük. Bu yazıda onlara isim vereceğiz. Başlamadan küçük bir soru: aşağıdaki kod derlenir mi? Hata verirse hangi satırda?
interface User {
name: string;
}
interface User {
age: number;
}
const ada: User = { name: "Ada" };
Aynı isimle iki ayrı interface yazdık. İlk tahminin "ikinci tanımda isim çakışması hatası" olabilir. Acele etme; cevabı declaration merging'i konuştuğumuz bölümde vereceğiz.
Aynı Şekli Her Yerde Tekrar Yazmak
Daha önce gördüğümüz inline object type'lar küçük örneklerde iş görür. Ama aynı şekil birden fazla yerde geçmeye başladığında sorun çıkar:
function printUser(user: { name: string; email: string; age?: number }) {
console.log(`${user.name} <${user.email}>`);
}
function isAdult(user: { name: string; email: string; age?: number }) {
return (user.age ?? 0) >= 18;
}
Bu iki fonksiyon aynı kullanıcı şeklini bekliyor, ama bunu bilen tek kişi sensin. TypeScript yalnızca iki ayrı yerde birbirine benzeyen iki tip görüyor. Yarın kullanıcıya bir phone property'si eklemen gerektiğinde her yeri tek tek bulup güncellemen gerekir; birini unutursan tutarsızlık sessizce koda yerleşir. Çözüm, bu şekle bir isim verip her yerde o ismi kullanmak. TypeScript bunun için iki araç sunar: type ve interface.
type Alias: Bir Tipe İsim Vermek
type User = {
name: string;
email: string;
age?: number;
};
function printUser(user: User) {
console.log(`${user.name} <${user.email}>`);
}
function isAdult(user: User) {
return (user.age ?? 0) >= 18;
}
type anahtar kelimesi, eşittir işaretinin sağındaki tipe bir isim verir. Buna type alias denir; "takma ad" anlamında. Optional ve readonly property'ler, index signature'lar, önceki yazıda gördüğümüz her şey aynen çalışır. Değişen tek şey, tipi bir kere yazıp her yerde ismini kullanman. Şekli değiştirmen gerektiğinde tek bir yeri değiştirirsin, TypeScript da o tipi kullanan her yeri yeniden denetler.
type alias'ın asıl gücü, yalnızca object'lerle sınırlı olmamasıdır. Eşittirin sağına herhangi bir tip yazabilirsin:
type Point = [number, number];
type Tags = readonly string[];
type UserId = string;
const center: Point = [0, 0];
const tags: Tags = ["typescript", "basics"];
const id: UserId = "u-42";
Burada önemli bir incelik var: alias yeni bir tip yaratmaz, var olan bir tipe ikinci bir isim verir. UserId ile string TypeScript'in gözünde tamamen aynı şeydir; UserId bekleyen bir yere herhangi bir string verebilirsin. İsim, kodu okuyan insan için anlam taşır, derleyici için değil. Gerçekten birbirinden ayırt edilen kimlik tiplerini İleri seride göreceğiz.
interface: Object'in Şeklini Tarif Etmek
interface Product {
readonly id: string;
title: string;
price: number;
discount?: number;
describe(): string;
}
const keyboard: Product = {
id: "p-1",
title: "Mechanical Keyboard",
price: 89,
describe() {
return `${this.title}: ${this.price}`;
},
};
interface yalnızca object şekilleri için vardır. Eşittir işareti yok; isimden sonra doğrudan süslü parantez gelir. İçeride property'leri ve method'ları yazarsın. describe(): string satırı, parametre almayan ve string döndüren bir method olduğunu söyler. Property'leri noktalı virgülle ayırmak gelenektir, virgül de kabul edilir.
Bu hâliyle type Product = { ... } ile interface Product { ... } arasında pratikte fark yok. İkisini de aynı yerde, aynı şekilde kullanırsın; editörde de, hata mesajlarında da aynı davranırlar. Farklar iki konuda ortaya çıkar: bir tipi genişletirken ve aynı ismi ikinci kez kullandığında.
Genişletmek: extends ve Intersection
Sık karşılaşılan bir ihtiyaç: var olan bir şekli alıp üzerine birkaç property eklemek. interface'te bunu extends ile yaparsın:
interface Entity {
id: string;
createdAt: Date;
}
interface Customer extends Entity {
name: string;
email: string;
}
const customer: Customer = {
id: "c-1",
createdAt: new Date(),
name: "Grace",
email: "grace@example.com",
};
Customer, Entity'nin bütün property'lerini alır ve kendi property'lerini ekler. Virgülle ayırarak birden fazla interface'ten de genişletebilirsin: interface Admin extends Customer, Auditable. Bir interface, object şekli tarif eden bir type alias'ı da extends edebilir; yani ikisini aynı projede karıştırmak sorun değildir.
type alias'ta extends anahtar kelimesi yok. Aynı sonucu intersection ile, & operatörüyle alırsın:
type Entity = {
id: string;
createdAt: Date;
};
type Customer = Entity & {
name: string;
email: string;
};
A & B, "hem A'nın hem B'nin bütün property'lerine sahip" demektir. Çoğu durumda iki yol aynı sonucu verir. Yolların ayrıldığı yer, iki taraf aynı property için uyuşmayan tipler söylediği zamandır:
interface Base {
id: string;
}
// Hata: Interface 'Item' incorrectly extends interface 'Base'.
interface Item extends Base {
id: number;
}
type BaseShape = { id: string };
type ItemShape = BaseShape & { id: number };
// Hata: Type 'number' is not assignable to type 'never'.
const item: ItemShape = { id: 1 };
extends çakışmayı tanımın yapıldığı yerde, açık bir mesajla yakalar: Item'ın id'si number, ama Base string istiyor. Intersection ise tanımda hiç itiraz etmez. id aynı anda hem string hem number olmak zorunda kalır; böyle bir değer olamayacağı için tipi never olur. Hatayı ancak o tipi kullanmaya çalıştığında, üstelik çok daha az şey anlatan bir mesajla görürsün. Intersection'ın ayrıntılarını ve gerçekten işe yaradığı durumları ayrı bir yazıda konuşacağız.
Declaration Merging: Aynı İsimde İki interface
Baştaki soruya dönelim. İkinci interface User tanımı hata vermez. Hata son satırdadır:
error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'User'.
Aynı scope'ta aynı isimle birden fazla interface yazdığında TypeScript bunları tek bir interface'te birleştirir. Buna declaration merging denir. İki tanım birleşince User hem name hem de age ister; biz ise yalnızca name verdik.
type alias'ta böyle bir birleşme yoktur. Aynı isimle ikinci kez type yazmak doğrudan hatadır ve TypeScript iki tanımı da işaretler:
// Hata: Duplicate identifier 'Account'.
type Account = { id: string };
// Hata: Duplicate identifier 'Account'.
type Account = { email: string };
Peki interface'lerin bu davranışı ne işe yarar? Asıl kullanım alanı, başkasının yazdığı tipleri genişletmektir. Örneğin tarayıcının global Window tipine kendi property'ni eklemek ya da bir library'nin tiplerinde eksik kalan bir alanı tamamlamak istediğinde, aynı isimde bir interface yazarsın ve TypeScript ikisini birleştirir. Bu tekniği declaration dosyalarını anlattığımız yazıda ayrıntılı göreceğiz. Günlük kodda ise declaration merging'i genellikle bilerek kullanmazsın; daha çok bilmeden başına gelir. Bunun nasıl olduğunu tuzaklar bölümünde göreceğiz.
Hangisini Ne Zaman Kullanmalı?
Bu soru TypeScript topluluğunda yıllardır tartışılıyor ve iki tarafın da makul gerekçeleri var. Pratik bir ayrım şöyle yapılabilir.
type alias'ın zorunlu olduğu yerler: union'lar (type Id = string | number), tuple'lar, primitive tiplere verilen isimler, fonksiyon tipleri ve ilerleyen serilerde göreceğimiz mapped ve conditional type'lar. interface bunların hiçbirini ifade edemez; yalnızca object şekli tarif eder.
interface'in öne çıktığı yerler: dışarıya açtığın, başkalarının genişletebileceği object şekilleri ve class'ların implements ile uyacağı sözleşmeler. Class'ları Orta seride ayrıntılı göreceğiz. extends çakışmaları erkenden yakalar ve hata mesajlarında interface'in adı görünür. TypeScript ekibinin performans önerileri de büyük projelerde intersection'la kurulan type'lar yerine extends ile kurulan interface'leri tercih etmeyi söyler, çünkü derleyici interface'ler arasındaki ilişkileri daha kolay önbelleğe alabilir.
Benim önerim basit: object şekilleri için interface, geri kalan her şey için type. Ama asıl kural tutarlılıktır. Ekibin hangisini seçtiyse projenin her yerinde aynısını kullan. "Bu dosyada interface, öbüründe type" karışıklığı, ikisi arasındaki küçük farklardan çok daha fazla zaman kaybettirir.
İsimlendirme
type ve interface isimleri PascalCase yazılır: User, OrderItem, PaymentStatus. Değişkenler ve fonksiyonlar camelCase olduğu için, büyük harfle başlayan bir isim gördüğünde bunun bir tip ya da class olduğunu hemen anlarsın.
C# ya da Java'dan gelenlerin alışkanlığı olan I öneki (IUser) TypeScript'te yaygın değildir; TypeScript ekibinin kendi kodlama kuralları da bu öneki kullanmamayı söyler. Benzer şekilde UserType ya da UserInterface gibi sonekler de bilgi eklemez; kodun kendisi zaten bunun bir tip olduğunu söylüyor. İsmi, tipin neyi temsil ettiğine göre seç: User bir kullanıcıdır, UserSummary listede gösterdiğin kısaltılmış hâlidir, NewUserInput bir formdan gelen ham veridir. İyi seçilmiş bir isim, tipin içine bakmadan ne işe yaradığını anlatır.
Sık Düşülen Tuzaklar
Birinci tuzak, bilmeden yapılan declaration merging. İçinde hiç import ya da export olmayan bir dosya TypeScript için bir script'tir ve içindeki tanımlar global scope'a girer. Global scope'ta ise tarayıcının DOM tipleri zaten durur. Şu koda bak:
interface Comment {
text: string;
}
// Hata: Type '{ text: string; }' is missing the following properties from type 'Comment': data, length, ownerDocument, appendData, and 59 more.
const comment: Comment = { text: "Nice post!" };
Sen bir blog yorumunu tarif ettiğini düşünüyorsun, ama DOM'da HTML'deki yorum düğümlerini temsil eden bir Comment interface'i zaten var. Seninki onunla birleşti ve şimdi altmışı aşkın property bekliyor. Response, Location, Text gibi isimlerde de aynı şey olabilir. Çözüm ya dosyayı module yapmak (en az bir import ya da export yeterli, gerekirse export {};) ya da daha belirgin bir isim seçmektir: BlogComment. Gerçek projelerde dosyaların neredeyse hepsi zaten module olduğu için bu tuzağa en çok tek dosyalık denemelerde düşersin. Aynı ismi type alias ile yazsaydın, TypeScript "Duplicate identifier 'Comment'" diyerek çakışmayı açıkça söylerdi.
İkinci tuzak, intersection'daki sessiz çakışma. Yukarıda gördük: extends uyuşmayan property'yi tanımda yakalar, & ise tanımda susar ve sorunu kullanım yerine erteler. Bir tipi başka tiplerden türetirken aynı isimli property'ler varsa interface ve extends daha güvenli bir yoldur.
Üçüncü tuzak, alias'ın yeni bir tip yarattığını sanmak. type Email = string ve type Url = string yazdın diye, bir e-posta adresini URL bekleyen fonksiyona göndermen engellenmez:
type Email = string;
type Url = string;
function openLink(url: Url) {
console.log(`Opening ${url}`);
}
const email: Email = "ada@example.com";
openLink(email); // hata yok: ikisi de sadece string
Alias'lar okunabilirlik içindir; tek başlarına ek bir tip güvenliği sağlamazlar.
Küçük Bir Deneme
★☆☆ Yazının başındaki printUser ve isAdult örneğini bir kez type User, bir kez interface User ile yaz. Editörde user parametresinin üzerine gel ve iki durumda gösterilen tipi karşılaştır.
★★☆ Derlenir mi, tahmin et: interface Settings { theme: string; } yazdıktan sonra aynı dosyaya interface Settings { theme: number; } eklersen ne olur? Sonra dene ve hata mesajını oku. Declaration merging'in de bir sınırı var.
★★★ Entity ve Customer örneğini al ve Customer'a bir de createdAt: string property'si ekle; önce interface ve extends ile, sonra type ve & ile. Hangisi hatayı nerede gösteriyor? Intersection'da createdAt'in type'ı ne oluyor, never mı, başka bir şey mi?
Bu yazıda tiplere isim vermeyi gördük ve type alias'ın union'ları da ifade edebildiğini söyledik. Bir sonraki yazıda tam olarak oraya bakacağız: bir değerin "ya bu ya şu" olabildiği durumları union ve literal tiplerle nasıl tarif ettiğimizi ve TypeScript'in bu ihtimallerle nasıl çalıştığını göreceğiz.