तुम्ही एक असा टाइप लिहिता जो नेस्टेड ऑब्जेक्ट्समधून फिरतो (walks through) आणि ऑटोकम्प्लीटसाठी डॉट-सेपरेटेड पाथ (dot-separated paths) तयार करतो. तो एका लहान टेस्ट ऑब्जेक्टवर उत्तमरित्या काम करतो. त्यानंतर तुम्ही तो एका खऱ्या API पेलोडवर वापरता आणि एडिटर फ्रीझ होतो. शेवटी, TypeScript TS2589: Type instantiation is excessively deep and possibly infinite. असा एरर देते.
या मेसेजचा अर्थ असा नाही की तुमच्या कोडमध्ये पारंपारिक अर्थाने इन्फिनिट लूप (infinite loop) आहे. याचा अर्थ असा आहे की कंपायलरने हार मानली आहे. तुम्ही ज्या टाइपची गणना करण्यास सांगितले होते, तो एकतर खरोखरच अमर्याद (unbounded) होता, किंवा मर्यादित होता पण इतका मोठा होता की त्याचे मूल्यांकन केल्यास TypeScript च्या अंतर्गत मर्यादा संपल्या असत्या. जेव्हा असे घडते, तेव्हा तुमचा IDE हँग होण्यापूर्वीच कंपायलर थांबतो.
TS2589 केव्हा दिसून येते
रिकर्सिव्ह टाइप्स (Recursive types) हे सर्वात सामान्य कारण आहेत. TypeScript टाइप्सचे मूल्यांकन वेगाने करते, आणि जर एखादा युटिलिटी टाइप स्वतःला वारंवार कॉल करत असेल—विशेषतः कंडिशनल लॉजिकद्वारे—तर कम्प्युटेशन स्टॅक वेगाने वाढतो. तुम्ही सहसा खालील काही विशिष्ट परिस्थितींमध्ये या समस्येचा सामना कराल:
- रिकर्सिव्ह कंडिशनल टाइप्स (Recursive conditional types) जे बेस केसपर्यंत पोहोचण्यासाठी टुपल (tuple), ऑब्जेक्ट किंवा स्ट्रिंग टेम्पलेटचे वारंवार डिकन्स्ट्रक्ट (destructure) करतात.
- खोलवर नेस्टेड ऑब्जेक्ट पाथ जनरेटर्स (Deeply nested object path generators), जे
{ user: { address: { street: string } } }सारख्या स्ट्रक्चर्सना"user" | "user.address" | "user.address.street"सारख्या स्ट्रिंग लिटरल्सच्या युनियनमध्ये रूपांतरित करतात. - टेम्पलेट लिटरल्स टाइप्स (Template literal types) जे स्ट्रिंग्सचे कॅरेक्टर किंवा टोकननुसार विश्लेषण करतात.
- मॅप्ड टाइप्स (Mapped types) जे डझनभर कीज (keys) आणि अनेक लेव्हल्स असलेल्या ऑब्जेक्ट्सवर इटरेट (iterate) होतात.
- कंडिशनल टाइप्स (Conditional types) जे मोठ्या युनियनवर डिस्ट्रिब्युट होतात, ज्यामुळे प्रत्येक मेंबरवर कामाचा भार मोठ्या प्रमाणात वाढतो.
नेस्टेड पाथचे उदाहरण विशेषतः मोहक आहे. फॉर्म लायब्ररी आणि स्टेट-मॅनेजमेंट टूल्सना फील्ड नावासाठी ऑटोकम्प्लीट मिळावा म्हणून टाइप केलेले पाथ्स (typed paths) ऑफर करायला आवडते. एका उथळ (shallow) ऑब्जेक्टवर, प्रत्येक वैध डॉट-पाथ स्ट्रिंग युनियन म्हणून जनरेट करणे सोपे असते. परंतु, एका खोल (deep) किंवा विस्तृत (wide) ऑब्जेक्टवर, ते युनियन अनियंत्रितपणे वाढते. TypeScript ला प्रत्येक पर्म्युटेशन (permutation) एकाच वेळी वर्किंग मेमरीमध्ये ठेवावे लागते. एका विशिष्ट खोलीवर, कंपायलरला जाणवते की काम त्याच्या बजेटपेक्षा वेगाने होत आहे आणि तो आपत्कालीन ब्रेक लावतो.
उपाय १: एक निश्चित डेप्थ लिमिट (Hard Depth Limit) जोडा
TS2589 सोडवण्याचा सर्वात थेट मार्ग म्हणजे तुमचा टाइप कायमस्वरूपी रिकर्सिव्ह होऊ शकतो असे भासवणे थांबवणे. एक डेप्थ काउंटर (depth counter) सुरू करा जो सर्किट ब्रेकरप्रमाणे काम करेल.
व्यवहारामध्ये, याचा अर्थ एक न्यूमेरिक जेनेरिक पॅरामीटर जोडणे असा आहे—ज्याला सहसा टुपल म्हणून दर्शवले जाते ज्याची लांबी कमी होत जाते—जो प्रत्येक वेळी टाइप रिकर्सिव्ह होताना कमी होतो. जेव्हा काउंटर शून्य होतो, तेव्हा टाइप अधिक खोलवर जाण्याऐवजी string सारखा एखादा ब्रॉड फॉलबॅक (broad fallback) रिटर्न करतो. वापरकर्त्यांना पहिल्या चार किंवा पाच लेव्हल्ससाठी अचूक ऑटोकम्प्लीट तरीही मिळेल, जे वास्तविक जगातील बहुतेक ऑब्जेक्ट्ससाठी पुरेसे असते. त्यापलीकडे, कंपायलर फक्त टाइपची व्याप्ती वाढवतो (widens the type) आणि पुढे जातो.
हा दृष्टिकोन तुमच्या युटिलिटी टाइपला कोणत्याही अर्थाने कमी अचूक बनवत नाही. तो फक्त त्याला मर्यादित (bounded) करतो. जो टाइप सिस्टम कंपायलर क्रॅश करते, ती सिस्टम एखाद्या योग्य खोलीनंतर सुव्यवस्थितपणे माघार घेणाऱ्या सिस्टमपेक्षा जास्त उपयुक्त नसते.
उपाय २: एका वेळी एकच पाथ व्हॅलिडेट करा
जर सर्व संभाव्य पाथ आधीच जनरेट करणे खूप खर्चिक (expensive) असेल, तर कॉन्ट्रॅक्ट बदला. सर्व वैध स्ट्रिंग्सचा एक मोठा युनियन तयार करण्याऐवजी, एखादी विशिष्ट स्ट्रिंग वैध पाथ आहे की नाही हे तपासणारा टाइप लिहा.
प्रत्येक इंग्रजी शब्दाचा शब्दकोश तयार करणे आणि एखादा शब्द स्पेलिंगनुसार बरोबर आहे की नाही हे तपासणे यातील फरकाचा विचार करा. पहिले एक प्रचंड डेटा स्ट्रक्चर आहे; दुसरे एक हलकी स्कॅनिंग प्रक्रिया आहे. TypeScript च्या भाषेत, "user.address.street" | "user.settings.theme" | ... देणारा Paths<T> युटिलिटी एक्सपोर्ट करण्याऐवजी, तुम्ही IsValidPath<T, "user.address.street"> सारखे काहीतरी एक्सपोर्ट करता. कंपायलर फक्त तुम्ही प्रत्यक्षात पास केलेला पाथ तपासतो.
हा बदल तुम्ही API कसे डिझाइन करता यावर परिणाम करतो. तुमचे फंक्शन सिग्नेचर्स कदाचित एक स्ट्रिंग स्वीकारतील आणि नंतर ऑब्जेक्ट शेपच्या विरुद्ध त्याची पडताळणी करण्यासाठी जेनेरिक कन्स्ट्रेंट (generic constraint) वापरतील. जर डेव्हलपरने चुकीचा पाथ टाईप केला तर IDE तक्रार करेलच, पण टाइप-चेकिंग दरम्यान कंपायलरला वैध पाथचा पूर्ण संच तयार करण्याची गरज पडणार नाही. मोठ्या ऑब्जेक्ट्ससाठी, कामगिरीतील (performance) फरक लक्षणीय असतो.
तुम्हाला पुढे नेणारे काही झटपट उपाय
या दोन स्ट्रक्चरल उपायांव्यतिरिक्त, काही लहान सवयी रिकर्सिव्ह टाइप्सना मर्यादेबाहेर जाण्यापासून रोखू शकतात:
वितरण रोखण्यासाठी टाईप पॅरामीटर्स ट्युपल्समध्ये (tuples) गुंडाळा.
Tहे युनियन (union) असताना,T extends Foo ? Bar : Bazसारख्या कंडिशनलमधील 'नेकेड' (naked) टाईप पॅरामीटर प्रत्येक सदस्यावर तपासणी वितरित करतो. जर त्या युनियनमध्ये पन्नास सदस्य असतील, तर TypeScript पन्नास स्वतंत्र इन्स्टँशिएशन्स (instantiations) करते.[T] extends [Foo] ? Bar : Bazअसे लिहिल्यामुळे संपूर्ण युनियनसाठी कंडिशनलची एकदाच तपासणी होते. जेव्हा तुम्हाला प्रत्येक युनियन सदस्यावर वैयक्तिकरित्या मॅप करण्याची गरज नसते, तेव्हा याचा वापर करा.डीबगिंग करताना तुमचे इनपुट्स कमी करा. जेव्हा TS2589 एरर येते, तेव्हा तुमच्या प्रोडक्शन ऑब्जेक्ट टाईपच्या जागी दोन प्रॉपर्टीज आणि एक लेव्हल नेस्टिंग असलेला एक छोटा स्टब (stub) वापरा. जर एरर निघून गेली, तर तुम्हाला खात्री पटेल की समस्या डेप्थ (depth) किंवा कार्डिनॅलिटीमध्ये (cardinality) आहे, सिंटॅक्समध्ये नाही. यामुळे स्ट्रक्चरलदृष्ट्या योग्य असलेल्या लॉजिकला पुन्हा लिहिण्याचा तुमचा वेळ वाचतो.
पब्लिक-फेसिंग API टाईप्स थोडे लवचिक ठेवा. अंतर्गत कामासाठी तुम्हाला अत्यंत अचूकतेची (surgical precision) गरज असू शकते. पण बाह्य वापरासाठी, परिपूर्णतेचा खर्च कधीकधी फायद्यापेक्षा जास्त असू शकतो. जर थोडे व्यापक (wider) autocomplete टाईपमुळे एडिटरमधील दोन सेकंदांचा लॅग (lag) टाळता येत असेल, तर तो बदल सहसा फायदेशीर ठरतो. टेस्टिंगमध्ये चुकीचे पाथ (bad paths) पकडण्यासाठी तुम्ही या लवचिक टाईपसोबत रनटाइम व्हॅलिडेटर (runtime validator) जोडू शकता.
TypeScript ही मर्यादा का लागू करते
TypeScript 'हॅल्टिंग प्रॉब्लेम' (halting problem) सोडवू शकत नाही. तुमचा रिकर्सिव्ह टाईप (recursive type) शेवटी थांबेल की अनंतकाळ चालू राहील, हे त्याला माहित नसते. कंपायलरमध्ये इन्फिनाइट लूपचा (infinite loop) धोका पत्करण्याऐवजी, ते एक सुरक्षित कटऑफ (conservative cutoff) लागू करते. कधीकधी तो कटऑफ अशा टाईपला थांबवतो जो पुरेसा वेळ दिला असता तर पूर्ण झाला असता. TS2589 म्हणजे कंपायलरने "चुकीपेक्षा सुरक्षितता महत्त्वाची" हे मान्य करणे आहे.
त्या मर्यादेचा आदर करणे हा प्रोडक्शन-ग्रेड टाईप्स लिहिण्याचा एक भाग आहे. टाईप डेफिनेशन म्हणजे कंपायलरमध्ये चालणारा कोड आहे, आणि महागड्या (expensive) कोडचे वास्तविक परिणाम होतात. स्लो रनटाइम कोड ज्याप्रमाणे युजर एक्सपिरियन्सवर परिणाम करतो, त्याचप्रमाणे स्लो ऑटोकम्प्लिट (autocomplete) डेव्हलपरच्या कामाच्या वेगावर (velocity) परिणाम करते.
मुख्य निष्कर्ष
TS2589 म्हणजे तुम्ही खराब टाईप-सिस्टम प्रोग्रामर आहात असा संकेत नाही. तर तुमचा टाईप एकाच वेळी खूप जास्त काम करत आहे, असा तो संकेत आहे. तुमच्या रिकर्शनला मर्यादा घाला (Cap your recursion), लेझी व्हॅलिडेशन (validate lazily) करा आणि अनावश्यक वितरणापासून (unnecessary distribution) संरक्षण करा. प्रगत टाईप्सचा (advanced types) उद्देश कंपाईल टाइममध्ये प्रत्येक संभाव्य सत्य सिद्ध करणे हा नाही; तर तुमच्या टीमला वेगवान आणि विश्वसनीय टूल्स देणे हा आहे. जो टाईप मिलिसेकंदात कंपाईल होतो आणि ९५% केसेस कव्हर करतो, तो सिद्धांतानुसार परिपूर्ण असणाऱ्या पण लँग्वेज सर्व्हर क्रॅश करणाऱ्या टाईपपेक्षा कितीतरी पटीने मौल्यवान आहे.
