TypeScript प्रोजेक्ट्स वाढत जातात. फाईल्सची संख्या वाढते. डिपेंडेंसीज (dependencies) गुंतागुंतीच्या होतात. आणि शेवटी, तुमचा बिल्ड (build) अशा ठिकाणी अडकतो ज्याचा तुमच्या लॉजिकच्या गुंतागुंतीशी काहीही संबंध नसतो, तर तो पूर्णपणे कंपायलरला (compiler) एक सिंगल डिक्लेरेशन फाईल (declaration file) लिहिण्यापूर्वी संपूर्ण विश्वाचा अभ्यास करावा लागतो, यावर अवलंबून असतो.

TypeScript 6.0 isolatedDeclarations द्वारे यावर उपाय शोधते. हे फीचर .d.ts फाईल्स कशा तयार केल्या जातात, याबद्दलचा विचार पुन्हा करते. डिक्लेरेशन एमिशनला (declaration emission) पूर्ण टाईप-चेकिंग पाइपलाइनशी जोडण्याऐवजी, हे फीचर कंपायलरला प्रत्येक सोर्स फाईल स्वतंत्रपणे (in isolation) पाहून त्या फाईल्स एमिट (emit) करण्याची परवानगी देते. याचा परिणाम असा होतो की, तुमचा बिल्ड प्रोसेस तुमच्या डिपेंडेंसी ग्राफमधून (dependency graph) एक-एक लिंक शोधण्याऐवजी, हजारो फाईल्सवर समांतर (parallel) चालू शकतो.

खरी अडचण (The Real Bottleneck)

सध्या, डिक्लेरेशन फाईल्स तयार करणे ही एक सिरीयल (serial) प्रक्रिया आहे. जेव्हा तुम्ही --declaration इनेबल करता आणि कंपायलर चालवता, तेव्हा जोपर्यंत TypeScript त्या मॉड्यूलला स्पर्श करणाऱ्या प्रत्येक टाईपला पूर्णपणे समजत नाही, तोपर्यंत ते एखाद्या विशिष्ट मॉड्यूलसाठी .d.ts फाईल एमिट करू शकत नाही. जर utils.ts मध्ये types.ts मधून टाईप्स इम्पोर्ट केले असतील, आणि types.ts मध्ये api.ts मधून काहीतरी घेतले असेल, तर utils.ts काय एक्सपोर्ट करते हे सांगण्यापूर्वी कंपायलरला ती साखळी (chain) सोडवावी लागते.

एका मोठ्या मोनोरेपोमध्ये (monorepo), ही साखळी अत्यंत त्रासदायक ठरू शकते. तुमच्या इम्पोर्ट ग्राफच्या रूटच्या जवळ असलेली एक सिंगल फाईल शेकडो डाउनस्ट्रीम फाईल्सच्या डिक्लेरेशन एमिशनला रोखू शकते. तुमच्या CPU मध्ये आठ कोअर्स (cores) आहेत, पण TypeScript पॅकेज बाउंड्रीजच्या पलीकडे प्रत्येक इंटरफेसचा आकार मेहनतीने पुन्हा तयार करत असताना त्यातील सात कोअर्स रिकामे बसलेले असतात. कंपायलर आवश्यक काम करत आहे, परंतु टाईप चेकिंग आणि डिक्लेरेशन एमिशनमधील कपलिंगमुळे (coupling), तुम्हाला फक्त पब्लिक सरफेस टाईप्स (public surface types) डिस्कवर लिहिण्यासाठी देखील क्रॉस-फाईल विश्लेषणाचा (cross-file analysis) पूर्ण खर्च सोसावा लागतो.

isolatedDeclarations नियम कसे बदलते

isolatedDeclarations हे कपलिंग तोडते. जेव्हा हा फ्लॅग इनेबल केला जातो, तेव्हा कंपायलर इतर कोणत्याही फाईलला विचार न करता सोर्स फाईलसाठी .d.ts फाईल एमिट करण्यास तयार होतो. हे करण्यासाठी तो एक साधा करार (contract) आवश्यक करतो: प्रत्येक एक्सपोर्टेड सिम्बॉलला (exported symbol) ज्या ठिकाणी तो डिक्लेअर केला आहे, तिथे एक स्पष्ट आणि दृश्य टाईप अॅनोटेशन (type annotation) असणे आवश्यक आहे.

जर कंपायलरला सोर्समध्ये लिहिलेला पूर्ण टाईप तिथेच दिसला, तर त्याला इन्फरन्स (inference) करण्याची गरज पडत नाही. त्याला इम्पोर्ट्स शोधण्याची गरज नसते. दुसऱ्या फाईलमधील User हे आयडेंटिफायर (identifier) इंटरफेस आहे, टाईप अलिआस (type alias) आहे की क्लास आहे, हे जाणून घेण्याची त्याला गरज नसते. तो फक्त तुम्ही जे लिहिले आहे तेच एमिट करतो.

याचा अर्थ असा की फाईल A आणि फाईल B त्यांच्या डिक्लेरेशन्स एकाच वेळी तयार करू शकतात. बिल्ड ऑर्केस्ट्रेटर (build orchestrator) प्रत्येक फाईल वेगळ्या थ्रेडला (thread) देऊ शकतो. ज्या फास्ट ट्रान्सपायलरना (fast transpilers) पूर्ण टाईप चेकर नसल्यामुळे पूर्वी .d.ts जनरेशन वगळले जात असे, ते आता डिक्लेरेशन फाईल्स देखील तयार करू शकतात, कारण हे काम आता पूर्णपणे सिंटॅक्टिक (syntactic) झाले आहे.

तडजोड: ते लिहून ठेवा (The Tradeoff: Write It Down)

वेग मोफत मिळत नाही. तुम्ही तुम्ही एक्सपोर्ट करत असलेल्या कोणत्याही गोष्टीसाठी टाईप इन्फरन्सवर (type inference) अवलंबून राहणे थांबवले पाहिजे. प्रत्येक पब्लिक फंक्शन, क्लास, व्हेरिएबल आणि कॉन्स्टंटसाठी त्याचा टाईप स्पष्टपणे लिहिलेला असणे आवश्यक आहे. जर TypeScript ला रिटर्न स्टेटमेंट पाहून किंवा जेनेरिक आर्ग्युमेंट (generic argument) सोडवून टाईप मोजावा लागला, तर isolatedDeclarations एरर (error) देईल.

प्रत्यक्ष व्यवहारात हे कसे दिसते ते खाली दिले आहे. फ्लॅगशिवाय, तुम्ही असे लिहू शकता:

export function fetchUser(id: number) {
  return fetch(`/users/${id}`).then(r => r.json());
}

TypeScript fetch, त्यानंतर Promise.prototype.then, आणि त्यानंतर r.json() रिटर्न करणाऱ्या अनायोनस फंक्शनचे (anonymous function) निरीक्षण करून रिटर्न टाईप इन्फर करतो. .d.ts एमिट करण्यासाठी, कंपायलरला हे सर्व विश्लेषण करावे लागते.

isolatedDeclarations इनेबल असताना, तुम्हाला एक्सपोर्टला अॅनोटेट (annotate) करावे लागेल:

interface User {
  id: number;
  email: string;
}

export function fetchUser(id: number): Promise<User> {
  return fetch(`/users/${id}`).then(r => r.json());
}

आता कंपायलरला लगेच Promise<User> दिसतो. तो डिक्लेरेशन एमिट करतो आणि पुढे जातो.

हा नियम व्यापकपणे लागू होतो. एक्सपोर्टेड ॲरे (arrays) साठी त्यांच्या एलिमेंट्सवरून इन्फर करण्याऐवजी स्पष्ट टाईप्सची आवश्यकता असते. एक्सपोर्टेड ऑब्जेक्ट्ससाठी, जर त्यांचा आकार (shape) वापरकर्त्यांसाठी महत्त्वाचा असेल, तर स्पष्ट टाईप अॅनोटेशन्सची आवश्यकता असते. जेनेरिक फंक्शन्ससाठी त्यांचे रिटर्न टाईप्स आणि कन्स्ट्रेंट्स (constraints) डिक्लेरेशन साइटवर दिसणे आवश्यक आहे. तुम्ही एखाद्या कॉम्प्लेक्स मॅप्ड टाईपचा (complex mapped type) निकाल पूर्णपणे लिहिलेल्या नेमड टाईप अलिआसशिवाय (named type alias) एक्सपोर्ट करू शकत नाही.

याचा फायदा असा आहे की तुमचा पब्लिक API स्वतःहून डॉक्युमेंटेड (self-documenting) होतो. वापरकर्ते—आणि कंपायलर—ला आता अंमलबजावणीच्या तपशीलावरून (implementation details) तुमचा हेतू शोधण्याची (reverse-engineer) गरज पडत नाही. टाईप्स हा एक जाणीवपूर्वक केलेला करार आहे.

वेळेचा परिणाम (Where the Time Goes)

मोठ्या कोडबेसमध्ये, याचा परिणाम त्वरित दिसून येतो. मिनिटांचा वेळ घेणारे बिल्ड टाइम्स आता सेकंदात कमी होऊ शकतात, कारण डिक्लेरेशन एमिशन आता प्रक्रियेतील सर्वात मोठा अडथळा राहत नाही. प्रत्येक फाईल स्वतंत्रपणे एमिट होते, त्यामुळे ही प्रक्रिया तुमच्याकडे असलेल्या कोअर्सच्या संख्येनुसार स्केल होते, तुमच्या इम्पोर्ट ग्राफच्या खोलीनुसार (depth) नाही.

यामुळे तुम्ही कोणते टूल्स वापरू शकता हे देखील बदलते. esbuild आणि swc सारखे transpilers आधीच TypeScript चे JavaScript मध्ये रूपांतर करण्यासाठी अत्यंत वेगवान आहेत, परंतु अनेक टीम्स अजूनही फक्त .d.ts फाइल्स तयार करण्यासाठी स्वतंत्रपणे tsc चालवतात. isolatedDeclarations मुळे, ही वेगवान टूल्स दोन्ही कामे करू शकतात. डिक्लेरेशन (declarations) तयार करण्यासाठी त्यांना TypeScript ची संपूर्ण टाईप सिस्टम पुन्हा तयार करण्याची गरज नसते; त्यांना फक्त सिंटॅक्स पार्स (parse) करण्याची आणि तुम्ही दिलेल्या explicit types कॉपी करण्याची गरज असते. यामुळे पर्यायी टूलचेनसह (toolchains) एंड-टू-एंड TypeScript बिल्ड्स अधिक व्यवहार्य होतात.

डिस्ट्रिब्युटेड (Distributed) आणि इन्क्रिमेंटल (incremental) बिल्ड्स देखील सोपे होतात. continuous integration मध्ये, रिमोट कॅशे (remote cache) किंवा शार्डेड बिल्ड (sharded build) पूर्ण transitive dependency graph आधी डाउनलोड न करता पॅकेजसाठी डिक्लेरेशन emit करू शकते. जर सोर्समध्ये types explicit असतील, तर बिल्ड शार्डकडे (build shard) आवश्यक असलेली सर्व माहिती उपलब्ध असते.

काय सारखेच राहते

ही मर्यादा फक्त exports ला लागू होते. एका मॉड्यूलच्या आत, सर्व काही नेहमीप्रमाणे सुरू राहते. लोकल व्हेरिएबल्स (local variables), प्रायव्हेट क्लास मेंबर्स (private class members) आणि अन-एक्सपोर्टेड हेल्पर फंक्शन्स (unexported helper functions) अजूनही पूर्ण type inference वर अवलंबून राहू शकतात. TypeScript कोणत्याही तक्रारीशिवाय लूप व्हेरिएबल किंवा closure पॅरामीटरचा टाईप आनंदाने infer करेल.

export function calculateTotal(items: Item[]): number {
  // Local variable: inference is fine
  const taxRate = 0.08;
  
  // Private class member inside a local class: inference is fine
  class Helper {
    private cache = new Map();
  }
  
  return items.reduce((sum, item) => sum + item.price * (1 + taxRate), 0);
}

फक्त एक्सपोर्ट केलेल्या फंक्शन सिग्नेचरला (function signature) annotation ची गरज होती. अंतर्गत यंत्रणा (internal machinery) लवचिक आणि expressive राहते. यामुळे कोडिंगचा (authoring) भार सहन करण्यायोग्य राहतो. तुम्ही सर्वत्र पूर्णपणे explicit स्टाईलमध्ये बदल करत नाही आहात; तुम्ही फक्त प्रत्येक मॉड्यूलच्या सीमेवर (boundary) करार (contract) औपचारिक (formalize) करत आहात.

हे तुमच्या codebase साठी योग्य आहे का?

isolatedDeclarations स्वीकारल्यामुळे तुमचा वेळ कुठे खर्च होतो यात बदल होतो. जेव्हा तुम्ही export लिहिता तेव्हा तुम्ही काही अतिरिक्त कीस्ट्रोक्स (keystrokes) खर्च करता आणि त्या बदल्यात तुम्हाला प्रत्येक बिल्डवर होणारा अतिरिक्त वेळ थांबतो. लायब्ररी लेखकांसाठी (library authors), हे सहसा सहज स्वीकारण्यासारखे असते. सार्वजनिक APIs ला तरी annotation असणे आवश्यकच आहे. क्लोज्ड monorepo मध्ये काम करणाऱ्या ॲप्लिकेशन डेव्हलपर्ससाठी, सुरुवातीचा हा खर्च अनावश्यक औपचारिकता वाटू शकतो. परंतु जर तुमची टीम बिल्ड वेळेची तुलना 'कॉफी ब्रेक'च्या वेळेशी करत असेल, तर हा बदल लवकरच फायदेशीर ठरेल.

तुम्ही हे टप्प्याटप्प्याने (incrementally) स्वीकारू शकता. फ्लॅग (flag) सक्षम करा, कंपायलर चालवा आणि एक्सपोर्ट केलेल्या सिम्बॉल्सवर (exported symbols) येणाऱ्या त्रुटी (errors) सुधारा. एरर मेसेजेस तुम्हाला नेमके सांगतील की कोणते public-facing types implicit आहेत. त्या सुधारा, अंतर्गत गोष्टींना तसेच ठेवा आणि तुमचा declaration स्टेप वेगवान होताना पहा.

एक गोष्ट लक्षात ठेवा: हा फ्लॅग स्वतः TypeScript च्या type checker ला वेगवान बनवत नाही. जर तुम्हाला तुमच्या एडिटरमध्ये जलद फीडबॅक हवा असेल किंवा tsc --noEmit वेगाने चालवायचे असेल, तर तुम्हाला अजूनही project references, अधिक कडक file inclusion किंवा इतर आर्किटेक्चरल सुधारणांची गरज आहे. isolatedDeclarations विशेषतः .d.ts फाइल्सच्या emission वर लक्ष केंद्रित करते. हे बिल्ड ऑप्टिमायझेशन (build optimization) आहे, टाईप-चेकिंग ऑप्टिमायझेशन (type-checking optimization) नाही.

मुख्य निष्कर्ष

isolatedDeclarations तुम्हाला तुमचे public types हे 'first-class artifacts' म्हणून मानण्यास सांगते. कंपायलरला ते शोधून काढण्यास (deduce) लावणे थांबवा. ते स्पष्टपणे लिहा. एकदा तुम्ही ते केले की, जेव्हा जेव्हा कंपायलरला डिक्लेरेशन फाईल तयार करण्याची गरज पडते, तेव्हा तो तुमच्या संपूर्ण dependency graph मध्ये शोध घेणे थांबवतो. तो समांतर (parallel) पद्धतीने emit करतो, esbuild आणि swc सारखी टूल्स पूर्ण TypeScript वर्कफ्लो हाताळतात आणि तुमचे monorepo बिल्ड्स संथ होणे थांबते.

खर्च बिल्ड वेळेकडून कोडिंग वेळेकडे (authoring time) सरकतो. बहुतेक वाढणाऱ्या टीम्ससाठी, हा एक फायदेशीर सौदा आहे.