TypeScript'te Type Inference ve Annotation: Tipi Ne Zaman Sen Yazarsın?
TypeScript'in type'ı nasıl kendisi çıkardığını, let ile const arasındaki widening farkını, parametrelere neden annotation yazıldığını ve dönüş tipini ne zaman açıkça yazman gerektiğini örneklerle görüyoruz.
Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.
Önce Tahmin Et
Bu yazıda neredeyse hiç annotation yazmadan başlıyoruz. Şu dört değişkene bak:
let city = "Ankara";
const country = "Turkey";
let count = 0;
city = "Izmir";
count = "ten";
İki soru soralım. Birincisi: bu kodda hata veren bir satır var mı, varsa hangisi? İkincisi daha ince: TypeScript city ve country için aynı type'ı mı çıkarır? İkisi de bir string ile başlıyor, ama biri let, diğeri const. Tahminlerini yap; ikisinin cevabı da birkaç bölüm sonra gelecek.
Type Inference: TypeScript Tipi Kendisi Çıkarır
Bir önceki yazıda kurduğumuz ortamda editörün bir özelliğinden bahsetmiştik: bir değişkenin üzerine geldiğinde TypeScript'in o değişken için bulduğu type'ı gösterir. Şimdi bu özelliği kullanacağız. Şu kodu editöre yaz ve her değişkenin üzerine tek tek gel:
const user = { name: "Ada", age: 36 };
// { name: string; age: number; }
const scores = [90, 85, 70];
// number[]
const tags = ["typescript", 2026];
// (string | number)[]
function getTotal(price: number, quantity: number) {
return price * quantity;
}
const total = getTotal(100, 2);
// number
Hiçbirine type yazmadık ama TypeScript hepsini biliyor. user object'inin içinde bir string ve bir number olduğunu, scores'un bir number array'i olduğunu, tags'in hem string hem number taşıyabilen bir array olduğunu kendisi buldu. Bu sonuncudaki dikey çizgi bir union; "string ya da number" demek ve ayrı bir yazıda ayrıntısıyla göreceğiz. getTotal için de dönüş tipini biz yazmadık: iki number'ın çarpımı number olduğu için fonksiyonun number döndürdüğünü, dolayısıyla total'ın da number olduğunu çıkardı.
Buna type inference deniyor. TypeScript bir değişkenin type'ını ona ilk verdiğin değerden çıkarır ve o andan sonra değişkenin bu type'a uymasını bekler. Tahmindeki count = "ten" satırı tam da bu yüzden hata verir. count değişkeni 0 ile başladığı için TypeScript onun type'ını number olarak çıkarmıştı:
error TS2322: Type 'string' is not assignable to type 'number'.
Zihinsel model şu: TypeScript, kodu senin yerine okuyan ve "bu değişken baştan beri sayıydı, şimdi neden metin oldu?" diye soran dikkatli bir meslektaş gibi. Ona her şeyi tek tek söylemen gerekmez; değerlere bakarak çoğunu kendisi anlar.
let ile const Arasındaki Fark: Widening
Gelelim tahmindeki ikinci soruya. city ve country'nin üzerine geldiğinde farklı şeyler görürsün:
let city = "Ankara";
// let city: string
const country = "Turkey";
// const country: "Turkey"
city'nin type'ı string, ama country'nin type'ı "Turkey". Bu ikincisi bir literal type: "herhangi bir string" değil, tam olarak "Turkey" değerini kabul eden bir type. Mantığı basit. const ile tanımlanan bir değişken bir daha değiştirilemez, dolayısıyla TypeScript onun değerini kesin olarak bilir. let ile tanımlanan değişkene ise sonradan başka bir string atanabilir; TypeScript bu yüzden type'ı "Ankara"'dan string'e genişletir. Bu genişletmeye widening deniyor. city = "Izmir" satırının sorunsuz derlenmesinin sebebi de bu.
Fark küçük görünür, ama bazen tam olarak belirli değerleri bekleyen bir fonksiyonla karşılaştığında ortaya çıkar:
function setTheme(value: "dark" | "light") {
console.log(`Theme: ${value}`);
}
const theme = "dark";
let mode = "dark";
setTheme(theme);
// Hata: Argument of type 'string' is not assignable to parameter of type '"dark" | "light"'.
setTheme(mode);
theme bir const olduğu için type'ı "dark" ve fonksiyona uyuyor. mode ise let; type'ı string'e genişlemiş ve her string "dark" ya da "light" değil. Literal type'lar ve union'lar ayrı bir yazının konusu; şimdilik let ile const arasındaki bu farkı akılda tutman yeterli.
Bir dikkat noktası daha: const sadece değişkeni sabitler, içindeki object'i değil. const settings = { mode: "dark" } yazarsan settings.mode'un type'ı yine string olur, çünkü JavaScript serilerinden bildiğin gibi bir object'in property'si sonradan değiştirilebilir.
Annotation Yazmak
Inference her zaman yetmez. Type'ı kendin söylemek istediğinde değişken adından sonra iki nokta ve type yazarsın. Buna type annotation denir:
let username: string = "ada";
let retries: number;
retries = 3;
// Hata: Type 'number' is not assignable to type 'string'.
username = 42;
retries örneği annotation'ın gerçekten işe yaradığı bir durum: değişkeni tanımladığın anda henüz bir değer yok, değer sonra gelecek. Başlangıç değeri olmadığında TypeScript'in bakacağı bir şey de olmaz, bu yüzden type'ı sen söylersin. username satırındaki annotation ise aslında gereksiz; birazdan bunu konuşacağız.
Fonksiyon Parametreleri: Annotation'ın Asıl Yeri
Inference'ın kendiliğinden çalışmadığı en önemli yer, fonksiyon parametreleridir:
// Hata: Parameter 'value' implicitly has an 'any' type.
function double(value) {
return value * 2;
}
"TypeScript fonksiyonun nasıl çağrıldığına bakıp anlayamaz mı?" diye düşünebilirsin. Anlayamaz, çünkü fonksiyon bir kez tanımlanır ve yüz farklı yerden, yüz farklı değerle çağrılabilir. TypeScript type'ları tanımdan çağrıya doğru akıtır, tersine değil. Parametrenin type'ı yazılmadığında elinde tek seçenek kalır: any, yani "her şey olabilir, hiçbir şeyi denetleme". strict modda bu sessizce olmaz, TypeScript hata verir. any'nin neden tehlikeli olduğunu bir sonraki yazıda konuşacağız. Şimdilik kural şu: fonksiyon parametrelerine annotation yazarsın, bu bir fonksiyonun dış dünyayla yaptığı sözleşmedir.
Bir istisna var. Bir fonksiyonu başka bir fonksiyona callback olarak verdiğinde TypeScript parametrenin type'ını bağlamdan çıkarır:
const prices = [100, 250, 40];
const doubled = prices.map((price) => price * 2);
// Hata: Property 'toUpperCase' does not exist on type 'number'.
const labels = prices.map((price) => price.toUpperCase());
Burada price'a annotation yazmadık, ama TypeScript prices'ın bir number array'i olduğunu ve map'in callback'e bu array'in element'lerini vereceğini biliyor. Bu yüzden price bir number ve toUpperCase çağrısı yakalanıyor. Buna contextual typing deniyor; callback'lerde annotation yazmana genelde gerek kalmaz.
Dönüş Tipi: Ne Zaman Yazmalı?
Dönüş tipini çoğu zaman TypeScript doğru çıkarır. Ama şu fonksiyona bak:
function getDiscount(total: number) {
if (total > 1000) {
return 0.1;
}
return "none";
}
// Hata: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
const finalPrice = 1200 * (1 - getDiscount(1200));
Fonksiyonu yazan kişi aslında hep bir sayı döndürmek istiyordu; ikinci return bir dalgınlık. TypeScript buna itiraz etmez, çünkü onun açısından fonksiyon gayet geçerli: dönüş tipi "0.1 ya da "none"" olarak çıkarılır. Hata ancak fonksiyonu kullandığın satırda patlar ve mesaj sana asıl sorunun nerede olduğunu söylemez. Gerçek bir projede bu satır başka bir dosyada, başka birinin kodunda olabilir.
Niyetini annotation ile yazdığında hata doğru yere taşınır:
function getDiscount(total: number): number {
if (total > 1000) {
return 0.1;
}
// Hata: Type 'string' is not assignable to type 'number'.
return "none";
}
Artık TypeScript sorunlu satırı doğrudan gösteriyor. Pratik kural şu: küçük, yerel fonksiyonlarda dönüş tipini inference'a bırak. Başka dosyaların kullandığı fonksiyonlarda, birden fazla return dalı olan fonksiyonlarda ve dönüş tipinin bir sözleşme olduğu her yerde açıkça yaz. Böylece fonksiyonun içini değiştiren biri, onu kullanan kodu farkında olmadan bozamaz.
Gereksiz ve Zararlı Annotation
TypeScript'e yeni başlayanlar genellikle her şeye type yazar:
const firstName: string = "Ada";
const price: number = 100;
const isActive: boolean = true;
Bunlar hatalı değil ama bilgi de eklemiyor; değer zaten type'ı söylüyor. Üstelik ilk satırda bir şey kaybettin: annotation olmasaydı firstName'in type'ı "Ada" olurdu, şimdi string. Annotation inference'ı ezer ve bazen ondan daha geniş bir type'a razı olmana yol açar.
Daha kötüsü, gereğinden geniş bir annotation'ın bildiğin bilgiyi silmesidir:
const user: object = { name: "Ada", age: 36 };
// Hata: Property 'name' does not exist on type 'object'.
console.log(user.name);
object "bu bir object" der ama içinde ne olduğunu söylemez. TypeScript annotation'a güvenir ve name'i unutur. Annotation'ı silsen, inference sana çok daha doğru bir type verirdi.
Akılda tutulacak denge şu: annotation'ı sınırlara yaz, yani fonksiyon parametrelerine, dışarıya açılan fonksiyonların dönüş tiplerine ve başlangıç değeri olmayan ya da boş başlayan değişkenlere (const cart: string[] = [] gibi). Fonksiyonların içindeki yerel değişkenleri inference'a bırak.
Sık Düşülen Tuzaklar
İlk tuzak, let ile tanımladığın bir değişkeni belirli literal değerler bekleyen bir yere vermek. Yukarıdaki setTheme(mode) örneğinde gördüğün gibi let type'ı genişletir. Değer değişmeyecekse const kullan.
İkincisi, hatayı susturmak için annotation'ı genişletmek. object ya da daha kötüsü any yazınca hata kaybolur, ama TypeScript'in sana verdiği bilgi de kaybolur. Bir hata gördüğünde önce type'ı daraltmayı dene, genişletmeyi değil.
Üçüncüsü, editörün gösterdiği type'ı kontrol etmeden varsaymak. "Bu bir string olmalı" diye düşündüğün her yerde üzerine gel ve bak. Bir değişkenin type'ını tahmin etmek yerine görmek, TypeScript öğrenmenin en hızlı yolu.
Küçük Bir Deneme
★☆☆ Tahmin et, sonra editörde üzerine gelerek kontrol et: const port = 8080;, let host = "localhost"; ve const server = { host: "localhost", port: 8080 }; için TypeScript hangi type'ları çıkarır?
★★☆ Derlenir mi? const ids = [1, 2, 3]; yazdıktan sonra ids.push("4"); satırını ekle. Hata verirse mesajı oku; array'in type'ını hangi değerden çıkardığını düşün.
★★★ getDiscount fonksiyonunu dönüş tipiyle birlikte yeniden yaz, öyle ki 1000'in altındaki tutarlar için 0 döndürsün. Sonra annotation'ı silip getDiscount'un üzerine gel: TypeScript şimdi hangi dönüş tipini çıkarıyor?
Şimdiye kadar string, number ve boolean gibi tanıdık type'larla çalıştık, ama bir de any gördük ve onu tehlikeli diye bir kenara koyduk. Bir sonraki yazıda temel tiplerin tamamını, null ve undefined'ın strict moddaki yerini ve any'nin güvenli kardeşi unknown ile birlikte void ve never'ı göreceğiz.