İç içe geçmiş nesneler arasında dolaşan ve otomatik tamamlama için nokta ile ayrılmış yollar oluşturan bir tip yazıyorsunuz. Küçük bir test nesnesi üzerinde harika çalışıyor. Ardından bunu gerçek bir API veri yüküne (payload) yönlendiriyorsunuz ve editör donuyor. Sonunda TypeScript şu hatayı veriyor: Type instantiation is excessively deep and possibly infinite.

Bu mesaj, kodunuzun geleneksel anlamda bir sonsuz döngü içerdiği anlamına gelmez. Derleyicinin pes ettiği anlamına gelir. Hesaplamasını istediğiniz tip ya gerçekten sınırsızdı ya da sonluydu ancak o kadar büyüktü ki değerlendirilmesi TypeScript'in dahili sınırlarını tüketirdi. Bu durum gerçekleştiğinde derleyici, IDE'nizi dondurmadan önce durur.

TS2589 Ne Zaman Görünür?

Özyinelemeli (recursive) tipler en yaygın suçludur. TypeScript tipleri eagerly (erken) değerlendirir ve eğer bir yardımcı tip —özellikle koşullu mantık aracılığıyla— kendini çağırmaya devam ederse, hesaplama yığını (computation stack) hızla büyür. Genellikle şu birkaç özel senaryoda bu engelle karşılaşırsınız:

  • Özyinelemeli koşullu tipler (Recursive conditional types); bir temel duruma ulaşana kadar bir demeti (tuple), nesneyi veya dize şablonunu (string template) tekrar tekrar parçalarına ayırır (destructure)
  • Derinlemesine iç içe geçmiş nesne yolu oluşturucular; { user: { address: { street: string } } } gibi yapıları "user" | "user.address" | "user.address.street" gibi dize sabiti birleşimlerine (string literal unions) dönüştürür
  • Template literal tipleri; dizeleri karakter karakter veya jeton (token) jeton ayrıştırır
  • Mapped tipler; onlarca anahtarı ve birden fazla seviyesi olan nesneler üzerinde yineleme yapar
  • Koşullu tipler; büyük birleşimler (unions) üzerinde dağılım yaparak iş yükünü her bir üye için sessizce katlar

İç içe geçmiş yol örneği özellikle caziptir. Form kütüphaneleri ve durum yönetimi (state-management) araçları, alan adları için otomatik tamamlama alabilmeniz adına tiplendirilmiş yollar sunmayı çok sever. Sığ bir nesnede, her geçerli nokta yolunu bir dize birleşimi olarak oluşturmak basittir. Derin veya geniş bir nesnede ise bu birleşim patlar. TypeScript, her permütasyonu aynı anda çalışma belleğinde tutmak zorundadır. Belirli bir derinlikte derleyici, iş yükünün bütçesini aştığını fark eder ve acil freni çeker.

Çözüm Bir: Katı Bir Derinlik Sınırı Ekleyin

TS2589'u çözmenin en doğrudan yolu, tipinizin sonsuza kadar özyineleme yapabileceği varsayımından vazgeçmektir. Bir devre kesici (circuit breaker) görevi gören bir derinlik sayacı ekleyin.

Uygulamada bu, tip her özyinelemeye girdiğinde azalan —genellikle uzunluğu geri sayan bir demet (tuple) olarak temsil edilen— sayısal bir generic parametre eklemek anlamına gelir. Sayaç sıfıra ulaştığında, tip daha derine inmek yerine string gibi geniş bir yedek (fallback) değer döndürür. Kullanıcılar, gerçek dünyadaki nesnelerin büyük çoğunluğunu kapsayan ilk dört veya beş seviye için hala hassas otomatik tamamlama alırlar. Bunun ötesinde, derleyici sadece tipi genişletir ve devam eder.

Bu yaklaşım, yardımcı tipinizi anlamlı bir şekilde daha az doğru yapmaz; onu sınırlandırır. Derleyiciyi çökerten bir tip sistemi, makul bir derinlikten sonra zarifçe geri adım atan bir sistemden daha kullanışlı değildir.

Çözüm İki: Her Seferinde Tek Bir Yolu Doğrulayın

Eğer tüm olası yolları en baştan oluşturmak çok maliyetliyse, sözleşmeyi (contract) değiştirin. Tüm geçerli dizelerin devasa bir birleşimini üretmek yerine, belirli bir dizenin geçerli bir yol olup olmadığını kontrol eden bir tip yazın.

Her İngilizce kelimenin sözlüğünü oluşturmak ile tek bir kelimenin doğru yazılıp yazılmadığını kontrol etmek arasındaki farkı düşünün. İlki devasa bir veri yapısıdır; ikincisi ise hafif bir taramadır. TypeScript terimleriyle, "user.address.street" | "user.settings.theme" | ... sonucunu veren bir Paths<T> yardımcısı dışa aktarmak yerine, IsValidPath<T, "user.address.street"> gibi bir şey dışa aktarırsınız. Derleyici yalnızca gerçekten geçtiğiniz yolu değerlendirir.

Bu değişim, API'leri nasıl tasarladığınızı değiştirir. Fonksiyon imzalarınız bir dize kabul edebilir ve ardından bunu nesne yapısına (object shape) göre doğrulamak için bir generic kısıtlaması (generic constraint) kullanabilir. Geliştirici hatalı bir yol yazarsa IDE hala şikayet eder, ancak derleyici tip kontrolü sırasında yasal yolların tam setini hiçbir zaman somutlaştırmak (materialize) zorunda kalmaz. Büyük nesneler için performans farkı çarpıcıdır.

Sizi İlerleten Hızlı Taktikler

İki yapısal çözümün ötesinde, birkaç küçük alışkanlık özyinelemeli tiplerin sınırı aşmasını engelleyebilir:

  • Dağıtımı engellemek için tip parametrelerini tuple içine alın. T bir union olduğunda, T extends Foo ? Bar : Baz gibi koşullu bir ifadedeki çıplak bir tip parametresi, kontrolü her bir üyeye dağıtır. Eğer bu union elli üyeye sahipse, TypeScript elli ayrı örnekleme (instantiation) gerçekleştirir. [T] extends [Foo] ? Bar : Baz yazmak, koşulu tüm union'a karşı tek bir seferde değerlendirir. Tipin her bir union üyesi üzerinde tek tek eşleşmesine (map) gerçekten ihtiyaç duymadığınız her durumda bunu kullanın.

  • Hata ayıklarken girdilerinizi küçültün. TS2589 hatası çıktığında, üretim (production) nesne tipinizi iki özelliği ve tek bir iç içe geçme (nesting) seviyesi olan küçük bir taslak (stub) ile değiştirin. Eğer hata kaybolursa, sorunun bir sözdizimi hatası değil, derinlik veya kardinalite (eleman sayısı) olduğunu doğrulamış olursunuz. Bu, yapısal olarak aslında doğru olan mantığı yeniden yazmaktan sizi kurtarır.

  • Dışarıya açık API tiplerini esnetin. Dahili olarak cerrahi bir hassasiyete ihtiyaç duyabilirsiniz. Dışarıdan bakıldığında, mükemmeliyet bazen sağladığı faydadan daha fazla maliyet çıkarır. Eğer biraz daha geniş bir autocomplete (otomatik tamamlama) tipi, editördeki iki saniyelik bir gecikmeyi önlüyorsa, bu takas genellikle buna değer. Testlerde hatalı yolları yakalamak için daha gevşek tipi bir çalışma zamanı (runtime) doğrulayıcısı ile eşleştirebilirsiniz.

TypeScript Neden Bu Sınırı Zorunlu Kılar

TypeScript durma problemini (halting problem) çözemez. Özyinelemeli (recursive) tipinizin eninde sonunda sonlanacağını mı yoksa sonsuza kadar döngüye mi gireceğini bilemez. Derleyici içinde sonsuz bir döngü riski almak yerine, muhafazakar bir kesme noktası uygular. Bazen bu kesme noktası, yeterli zaman verilseydi sonlanacak olan bir tipi yakalar. TS2589, derleyicinin hata yapmaktansa tedbirli olmayı tercih ettiğinin bir itirafıdır.

Bu sınıra saygı duymak, üretim seviyesinde tipler yazmanın bir parçasıdır. Bir tip tanımı derleyicide çalışan bir koddur ve maliyetli kodun gerçek sonuçları vardır. Yavaş autocomplete, yavaş çalışma zamanı kodunun kullanıcı deneyimine zarar verdiği kadar geliştirici hızına da zarar verir.

Asıl Çıkarılması Gereken Ders

TS2589, kötü bir tip sistemi programcısı olduğunuzun bir sinyali değildir. Tipinizin aynı anda çok fazla iş yapmaya çalıştığının bir sinyalidir. Özyinelemeyi sınırlayın, doğrulamayı erteleyerek (lazy) yapın ve gereksiz dağılıma karşı önlem alın. Gelişmiş tiplerin amacı, derleme zamanında her olası gerçeği kanıtlamak değil; ekibinize hızlı ve güvenilir araçlar sağlamaktır. Milisaniyeler içinde derlenen ve vakaların yüzde doksan beşini kapsayan bir tip, teorik olarak mükemmel olan ancak dil sunucusunu (language server) çökerten bir tipten çok daha değerlidir.