TypeScript'te Object Tipleri: Optional, readonly Property ve Index Signature
Inline object type'ları, optional ve readonly property'leri, index signature'ı, Record'u ve fazla property kontrolünün fresh object literal kuralını, yani TypeScript'in fazla property'yi neden bazen yakaladığını görüyoruz.
Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.
Önce Tahmin Et
Bir kullanıcıyı ekrana yazdıran küçük bir fonksiyon. Parametresinin type'ı, adı ve yaşı olan bir object:
function printUser(user: { name: string; age: number }) {
console.log(`${user.name} (${user.age})`);
}
const ada = { name: "Ada", age: 36, email: "ada@example.com" };
printUser(ada);
printUser({ name: "Grace", age: 45, email: "grace@example.com" });
İki çağrı da fonksiyonun beklemediği bir email property'si taşıyor. Biri değişken üzerinden, diğeri doğrudan yazılmış bir object ile. Sence ikisi de derlenir mi, ikisi de hata mı verir, yoksa biri geçip diğeri takılır mı? Takılırsa hangisi? Cevap, "fazla property kontrolü" bölümünde.
Inline Object Type
Yukarıdaki parametrede gördüğün { name: string; age: number } bir object type. Bir object'in hangi property'lere sahip olduğunu ve her birinin type'ını söyler. Bir type'a isim vermeden, kullanıldığı yerde yazdığımız için buna inline object type diyoruz. Property'leri noktalı virgülle ya da virgülle ayırabilirsin; ikisi de geçerli.
TypeScript, bu type'ı bekleyen bir yere verdiğin object'i property property denetler. Eksik bir property varsa hemen söyler:
function printUser(user: { name: string; age: number }) {
console.log(`${user.name} (${user.age})`);
}
// Hata: Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'.
printUser({ name: "Alan" });
Hata mesajı okumayı ortamı kurduğumuz yazıdan hatırlıyorsun: önce verdiğin şeyin type'ı ({ name: string; }), sonra beklenen type geliyor ve aradaki fark açıkça yazıyor. Object type'ları fonksiyon parametrelerinde, değişken annotation'larında ve dönüş tiplerinde aynı şekilde kullanılır.
Optional Property
Her kullanıcının e-posta adresi olmayabilir. Property adının sonuna ? koyarsan o property'nin hiç olmayabileceğini söylersin:
function sendWelcome(user: { name: string; email?: string }) {
// Hata: 'user.email' is possibly 'undefined'.
console.log(user.email.toLowerCase());
if (user.email !== undefined) {
console.log(`Sending to ${user.email.toLowerCase()}`);
}
}
sendWelcome({ name: "Ada" });
sendWelcome({ name: "Grace", email: "Grace@Example.com" });
// Sending to grace@example.com
İki çağrı da geçerli. Fonksiyonun içinde ise user.email'in type'ı string | undefined olur. Bir önceki yazılarda gördüğümüz kural burada da geçerli: TypeScript, kontrol etmeden toLowerCase çağırmana izin vermez. if ile kontrol ettikten sonra blok içinde değerin bir string olduğunu bilir.
Ortamı kurarken tsc --init'in exactOptionalPropertyTypes ayarını açtığını görmüştük. Bu ayar açıkken email?: string "bu property hiç yazılmayabilir" anlamına gelir, "undefined değeri alabilir" anlamına değil. Yani sendWelcome({ name: "Grace", email: undefined }) çağrısı bu ayarla TS2379 hatası verir. Ayar kapalıyken ise iki durum arasında fark yoktur. Projende hangisinin geçerli olduğunu bilmen, bu tür hataları gördüğünde şaşırmamanı sağlar.
readonly Property
Bazı property'ler oluşturulduktan sonra değişmemeli; bir kaydın kimliği gibi. Property adının önüne readonly yazarsın:
const product: { readonly id: number; title: string; tags: string[] } = {
id: 1,
title: "Keyboard",
tags: [],
};
product.title = "Mechanical Keyboard";
// Hata: Cannot assign to 'id' because it is a read-only property.
product.id = 2;
product.tags.push("sale");
title değiştirilebilir, id değiştirilemez. Son satıra dikkat: tags'e yeni bir element eklemek sorunsuz. readonly sadece property'nin kendisine yeniden atama yapılmasını engeller, içindeki array'in ya da object'in değişmesini değil. Yani sığdır; JavaScript serilerinde Object.freeze için konuştuğumuz sığlığın aynısı. tags'in de değişmemesini istiyorsan type'ını bir önceki yazıdaki gibi readonly string[] yaparsın. Ve yine unutma: readonly çalışma zamanında hiçbir şey yapmaz, sadece TypeScript'in izin verdiği işlemleri kısıtlar.
Index Signature: Key'leri Önceden Bilinmeyen Object'ler
Şu ana kadar property adlarını önceden biliyorduk. Peki ürün kodlarını stok adedine eşleyen bir object'in key'lerini nasıl yazarsın? Kodlar veriden gelir, sayıları da belli değildir. Bunun için index signature kullanılır:
const stock: { [sku: string]: number } = {
"KB-01": 12,
"MS-02": 0,
};
stock["HD-03"] = 5;
const count = stock["XX-99"];
// const count: number
console.log(count.toFixed(0));
[sku: string]: number, "string olan her key için value bir number" demek. Köşeli parantezin içindeki sku sadece okunabilirlik için bir etiket; yerine key da yazabilirdin. Artık istediğin key ile okuma ve yazma yapabilirsin ve TypeScript value'nun bir number olduğunu bilir.
Ama son satırdaki sorunu görüyor musun? "XX-99" diye bir key yok. TypeScript yine de count'un type'ını number olarak gösteriyor ve kod çalıştığında "Cannot read properties of undefined (reading 'toFixed')" hatası alıyorsun. Bu, bir önceki yazıda array index'leri için gördüğümüz durumun aynısı ve çözümü de aynı: noUncheckedIndexedAccess açıkken count'un type'ı number | undefined olur ve TypeScript kontrol yazmanı ister.
Index signature ile bilinen property'leri birlikte yazabilirsin, ama bir şartla: bilinen property'ler de index signature'a uymalı.
// Hata: Property 'label' of type 'string' is not assignable to 'string' index type 'number'.
function report(data: { [sku: string]: number; label: string }) {
console.log(data.label);
}
Mantığı şu: "her string key'in value'su number" dedin, label da bir string key ama value'su string. İki kural çelişiyor.
Fazla Property Kontrolü ve Fresh Object Literal
Tahmindeki soruya gelelim. printUser(ada) derlenir, ama ikinci çağrı şu hatayı verir:
error TS2353: Object literal may only specify known properties, and 'email' does not exist in type '{ name: string; age: number; }'.
Aynı property, aynı fonksiyon; neden farklı sonuç? TypeScript'in tip sistemi genel olarak "en az şunlar olsun" mantığıyla çalışır. ada değişkeninde name ve age var; fazladan bir email olması fonksiyonu bozmaz, çünkü fonksiyon onu zaten okumuyor. Bu esnekliğin adı structural typing ve ona ayrı bir yazı ayıracağız.
Ama doğrudan yazılmış bir object literal'ı, yani henüz hiçbir değişkene atanmamış "taze" (fresh) bir object'i bir type'a verdiğinde TypeScript ek bir kontrol yapar: bu type'ta olmayan bir property yazdıysan büyük ihtimalle bir hata yapmışsındır. Bu object'i başka hiçbir yer kullanmıyor, dolayısıyla fazladan property'nin bir amacı olamaz. Bu kontrolün en büyük faydası yazım hatalarını yakalamak:
function sendWelcome(user: { name: string; email?: string }) {
console.log(user.name);
}
// Hata: Object literal may only specify known properties, but 'emial' does not exist in type '{ name: string; email?: string | undefined; }'. Did you mean to write 'email'?
sendWelcome({ name: "Grace", emial: "grace@example.com" });
email optional olduğu için bu kontrol olmasaydı emial sessizce geçer, e-posta hiç gönderilmezdi. TypeScript sadece hatayı bulmakla kalmıyor, doğrusunu da öneriyor. Aynı kontrol annotation'lı bir değişkene object literal atadığında da çalışır: const grace: { name: string; age: number } = { name: "Grace", age: 45, email: "..." } de aynı hatayı verir.
Bu kontrolü atlatmak için object'i önce bir değişkene atamak teknik olarak mümkün, ama bunu hatayı susturmak için yapma. Fazla property hatası çoğu zaman gerçekten bir yanlışlığın ya da type'ın eksik olduğunun işaretidir.
Record'a Kısa Bir Bakış
Index signature yazmanın daha kısa bir yolu var: Record.
const prices: Record<string, number> = { "KB-01": 49.9 };
// Hata: Property 'dark' is missing in type '{ light: string; }' but required in type 'Record<"dark" | "light", string>'.
const themeLabels: Record<"light" | "dark", string> = {
light: "Light mode",
};
Record<string, number>, { [key: string]: number } ile aynı anlama gelir. Asıl gücü ikinci örnekte: key olarak belirli literal'lar verdiğinde her birinin var olmasını şart koşar. Record TypeScript'in hazır utility type'larından biri; onları Orta seride, nasıl çalıştıklarıyla birlikte ayrıntılı göreceğiz.
Sık Düşülen Tuzaklar
Birincisi, {} type'ını "boş object" sanmak. {} aslında "null ve undefined dışındaki her şey" demek. const anything: {} = 42; sorunsuz derlenir; sadece null ve undefined reddedilir. Bunun yerine object yazmak da çoğu zaman çözüm değil; type inference yazısında object annotation'ının bilgiyi nasıl sildiğini görmüştük. Object'in içinde ne olduğunu yaz.
İkincisi, aynı inline type'ı her yerde tekrar yazmak. { name: string; email?: string } üç fonksiyonda geçiyorsa bir property eklemek istediğinde üç yeri değiştirmen gerekir. Type'lara isim vermek bunun çözümü ve bir sonraki yazının konusu.
Üçüncüsü, index signature'ın her key'in var olduğu yanılsamasını vermesi. stock["XX-99"] örneğinde gördüğün gibi TypeScript olmayan bir key'i de var sayar. Bu tür object'lerde ya noUncheckedIndexedAccess aç ya da okuduğun değeri kontrol et.
Küçük Bir Deneme
★☆☆ Derlenir mi? const point: { x: number; y: number } = { x: 1, y: 2, z: 3 }; Peki const raw = { x: 1, y: 2, z: 3 }; yazıp ardından const point: { x: number; y: number } = raw; dersen?
★★☆ Bir blog yazısını temsil eden bir object type yaz: değişmeyen bir id (number), bir title (string), isteğe bağlı bir subtitle (string) ve değiştirilemeyen bir tags array'i. id'yi değiştirmeyi ve tags'e element eklemeyi dene.
★★★ Kelimeleri sayan bir fonksiyon yaz: bir string array'i alsın ve Record<string, number> döndürsün, her kelimenin kaç kez geçtiğini tutsun. Sonra aynı kodu noUncheckedIndexedAccess açıkken derle ve hangi satırın değişmesi gerektiğine bak.
Bu yazıdaki object type'ları hep kullanıldıkları yerde yazdık ve bunun bir sınırı olduğunu gördük. Bir sonraki yazıda type'lara isim vermenin iki yolunu, type alias ve interface'i yan yana koyacağız: nasıl yazılırlar, birbirlerinden nerede ayrılırlar ve hangisini ne zaman seçmelisin.