Anda menulis satu type yang menelusuri objek bersarang, membina laluan berasingan titik untuk fungsi autocomplete. Ia berfungsi dengan baik pada objek ujian yang kecil. Kemudian, anda menggunakannya pada muatan (payload) API yang sebenar, dan editor menjadi beku. Akhirnya, TypeScript mengeluarkan ralat TS2589: Type instantiation is excessively deep and possibly infinite.
Mesej ini tidak bermakna kod anda mengandungi gelung infiniti (infinite loop) dalam pengertian tradisional. Ia bermakna pengkompil (compiler) telah berputus asa. type yang anda minta untuk dikira sama ada benar-benar tidak terhad, atau terhad tetapi terlalu besar sehingga penilaiannya akan menghabiskan had dalaman TypeScript. Apabila ini berlaku, pengkompil akan berhenti sebelum ia menyebabkan IDE anda tergantung.
Apabila TS2589 Muncul
Jenis rekursif adalah punca yang paling biasa. TypeScript menilai type secara agresif, dan jika satu utility type terus memanggil dirinya sendiri—terutamanya melalui logik bersyarat—timbunan pengiraan (computation stack) akan berkembang dengan cepat. Anda biasanya akan menghadapi masalah ini dalam beberapa senario khusus:
- Jenis bersyarat rekursif yang berulang kali melakukan destructure pada tuple, objek, atau templat string sehingga mencapai kes asas (base case)
- Penjana laluan objek bersarang yang mendalam, yang menukarkan struktur seperti
{ user: { address: { street: string } } }kepada kesatuan (unions) literal string seperti"user" | "user.address" | "user.address.street" - Jenis literal templat yang menganalisis string aksara demi aksara atau token demi token
- Jenis dipetakan (Mapped types) yang diulang melalui objek dengan berpuluh-puluh kunci dan pelbagai tahap
- Jenis bersyarat yang diagihkan ke atas kesatuan (unions) yang besar, secara senyap menggandakan beban kerja pada setiap ahli
Contoh laluan bersarang adalah sangat menggoda. Pustaka borang (form libraries) dan alat pengurusan keadaan (state-management tools) gemar menawarkan laluan bertipe supaya anda mendapat autocomplete untuk nama medan. Pada objek yang cetek, menjana setiap laluan titik yang sah sebagai kesatuan string adalah mudah. Pada objek yang dalam atau luas, kesatuan tersebut akan meledak. TypeScript mesti memegang setiap permutasi dalam memori kerja secara serentak. Pada kedalaman tertentu, pengkompil menyedari bahawa kerja tersebut melebihi bajetnya dan menarik brek kecemasan.
Penyelesaian Satu: Tambah Had Kedalaman Tegas
Cara paling langsung untuk menyelesaikan TS2589 adalah dengan berhenti berpura-pura bahawa type anda boleh melakukan rekursi selama-lamanya. Perkenalkan pengira kedalaman yang bertindak sebagai pemutus litar (circuit breaker).
Dalam praktiknya, ini bermakna menambah parameter generik berangka—sering diwakili sebagai tuple yang panjangnya dikira secara menurun—yang berkurangan setiap kali type tersebut melakukan rekursi. Apabila pengira mencapai sifar, type tersebut akan mengembalikan sandaran (fallback) yang luas seperti string dan bukannya menggali lebih dalam. Pengguna masih mendapat autocomplete yang tepat untuk empat atau lima tahap pertama, yang merangkumi sebahagian besar objek dunia sebenar. Melebihi itu, pengkompil hanya akan meluaskan type tersebut dan meneruskan proses.
Pendekatan ini tidak menjadikan utility type anda kurang tepat dalam sebarang cara yang bermakna. Ia menjadikannya terhad. Sistem jenis yang merosakkan pengkompil tidak lebih berguna daripada sistem yang mengalah secara budiman selepas kedalaman yang munasabah.
Penyelesaian Dua: Sahkan Satu Laluan pada Satu Masa
Jika menjana setiap laluan yang mungkin secara awal adalah terlalu mahal, ubah kontraknya. Daripada menghasilkan kesatuan (union) besar bagi semua string yang sah, tulis satu type yang menyemak sama ada satu string khusus adalah laluan yang sah.
Fikirkan tentang perbezaan antara mencipta kamus bagi setiap perkataan bahasa Inggeris berbanding menyemak jika satu perkataan dieja dengan betul. Yang pertama adalah struktur data yang besar; yang terakhir adalah imbasan yang ringan. Dalam istilah TypeScript, daripada mengeksport utiliti Paths<T> yang menghasilkan "user.address.street" | "user.settings.theme" | ..., anda mengeksport sesuatu seperti IsValidPath<T, "user.address.street">. Pengkompil hanya menilai laluan yang anda masukkan.
Peralihan ini mengubah cara anda mereka bentuk API. Tandatangan fungsi anda mungkin menerima string dan kemudian menggunakan kekangan generik untuk mengesahkannya terhadap bentuk objek. IDE masih akan merungut jika pembangun menaip laluan yang salah, tetapi pengkompil tidak perlu menghasilkan set lengkap laluan yang sah semasa pemeriksaan jenis. Untuk objek yang besar, perbezaan prestasinya adalah sangat ketara.
Taktik Pantas untuk Terus Maju
Selain daripada dua penyelesaian struktur tersebut, beberapa tabiat kecil boleh menghalang jenis rekursif daripada melampaui batas:
Bungkus parameter jenis dalam tuple untuk menyekat pengagihan. Parameter jenis tanpa pembungkus dalam syarat, seperti
T extends Foo ? Bar : Baz, mengagihkan semakan ke setiap ahli apabilaTialah satu union. Jika union tersebut mempunyai lima puluh ahli, TypeScript akan melakukan lima puluh penginstansian berasingan. Menulis[T] extends [Foo] ? Bar : Bazmenilai syarat tersebut sekali sahaja terhadap keseluruhan union. Gunakan ini apabila anda sebenarnya tidak memerlukan jenis tersebut untuk memetakan setiap ahli union secara individu.Kecilkan input anda semasa menyahpepijat. Apabila TS2589 muncul, tukar jenis objek pengeluaran anda kepada stub kecil dengan dua sifat dan satu tahap nesting. Jika ralat tersebut hilang, anda telah mengesahkan bahawa kedalaman atau kardinaliti adalah puncanya, bukannya kesilapan sintaks. Ini menyelamatkan anda daripada menulis semula logik yang sebenarnya sudah betul dari segi struktur.
Lembutkan jenis API yang menghadap awam. Secara dalaman, anda mungkin memerlukan ketepatan yang jitu. Secara luaran, kesempurnaan kadangkala menelan kos yang lebih tinggi daripada manfaatnya. Jika jenis autocomplete yang sedikit lebih luas dapat mengelakkan lengah selama dua saat dalam editor, pertukaran itu biasanya berbaloi. Anda boleh menggabungkan jenis yang lebih longgar itu dengan pengesah masa larian (runtime validator) untuk mengesan laluan yang salah semasa ujian.
Mengapa TypeScript Menguatkuasakan Sempadan Ini
TypeScript tidak dapat menyelesaikan masalah pemberhentian (halting problem). Ia tidak tahu sama ada jenis rekursif anda akhirnya akan tamat atau berputar selama-lamanya. Daripada mengambil risiko gelung infiniti di dalam pengkompil, ia menguatkuasakan had pemotongan yang konservatif. Kadangkala pemotongan itu menangkap jenis yang sepatutnya sudah selesai, jika diberi masa yang mencukupi. TS2589 adalah pengakuan pengkompil bahawa ia lebih rela berwaspada daripada menyesal kemudian.
Menghormati had tersebut adalah sebahagian daripada penulisan jenis gred pengeluaran. Definisi jenis adalah kod yang berjalan dalam pengkompil, dan kod yang mahal mempunyai kesan yang nyata. Autocomplete yang perlahan menjejaskan kepantasan pembangun sama seperti kod masa larian yang perlahan menjejaskan pengalaman pengguna.
Rumusan Sebenar
TS2589 bukanlah isyarat bahawa anda seorang pengatur cara sistem jenis yang lemah. Ia adalah isyarat bahawa jenis anda melakukan terlalu banyak kerja dalam satu masa. Hadkan rekursi anda, sahkan secara malas (lazily), dan lindungi daripada pengagihan yang tidak perlu. Matlamat jenis lanjutan bukanlah untuk membuktikan setiap kebenaran yang mungkin pada masa pengkompilan; ia adalah untuk memberikan peralatan yang pantas dan boleh dipercayai kepada pasukan anda. Jenis yang dikompil dalam milisaat dan merangkumi sembilan puluh lima peratus kes adalah jauh lebih berharga daripada jenis yang sempurna secara teori tetapi menyebabkan pelayan bahasa (language server) terhenti.
