TypeScript پروجیکٹس بڑھتے ہیں۔ فائلیں کثرت سے بڑھتی ہیں۔ ڈیپینڈنسیز (dependencies) الجھ جاتی ہیں۔ اور آخر کار، آپ کا بلڈ (build) ایک ایسی دیوار سے ٹکرا جاتا ہے جس کا آپ کے لاجک کی پیچیدگی سے کوئی تعلق نہیں ہوتا، بلکہ اس کا تعلق اس حقیقت سے ہوتا ہے کہ کمپائلر کو ایک واحد ڈیکلیریشن فائل لکھنے سے پہلے پوری کائنات کو پڑھنے کی ضرورت پڑتی ہے۔
TypeScript 6.0 اس مسئلے کو isolatedDeclarations کے ذریعے حل کرتا ہے۔ یہ فیچر اس بات پر نئے سرے سے غور کرتا ہے کہ .d.ts فائلیں کیسے بنتی ہیں۔ ڈیکلیریشن ایمیشن (declaration emission) کو مکمل ٹائپ چیکنگ پائپ لائن سے جوڑنے کے بجائے، یہ کمپائلر کو ہر سورس فائل کو علیحدگی سے دیکھ کر وہ فائلیں جاری کرنے کی اجازت دیتا ہے۔ اس کا نتیجہ ایک ایسا بلڈ پروسیس ہے جو آپ کے ڈیپینڈنسی گراف (dependency graph) میں ایک ایک لنک کر کے چلنے کے بجائے ہزاروں فائلوں پر متوازی (parallel) طور پر چل سکتا ہے۔
اصل رکاوٹ (The Real Bottleneck)
اس وقت، ڈیکلیریشن فائلیں تیار کرنا ایک سیریل (serial) آپریشن ہے۔ جب آپ --declaration کو فعال کرتے ہیں اور کمپائلر چلاتے ہیں، تو TypeScript کسی مخصوص ماڈیول کے لیے .d.ts فائل تب تک جاری نہیں کر سکتا جب تک وہ اس ماڈیول سے جڑے ہر ٹائپ کو مکمل طور پر نہ سمجھ لے۔ اگر utils.ts میں types.ts سے ٹائپس امپورٹ کی گئی ہیں، اور types.ts میں api.ts سے کچھ لیا گیا ہے، تو کمپائلر کو یہ بتانے سے پہلے کہ utils.ts کیا ایکسپورٹ کرتا ہے، اس پوری زنجیر کو حل کرنا ہوگا۔
ایک بڑے مونو ریپو (monorepo) میں، یہ سلسلہ انتہائی تکلیف دہ ہوتا ہے۔ آپ کے امپورٹ گراف کے قریب موجود ایک اکیلی فائل سینکڑوں ڈاؤن اسٹریم (downstream) فائلوں کے لیے ڈیکلیریشن ایمیشن کو روک سکتی ہے۔ آپ کے CPU میں آٹھ کور (cores) ہیں، لیکن ان میں سے سات فار بیٹھے رہتے ہیں جبکہ TypeScript محنت سے ہر انٹرفیس کی شکل کو پیکیج کی حدود کے پار دوبارہ ترتیب دیتا ہے۔ کمپائلر ضروری کام کر رہا ہے، لیکن ٹائپ چیکنگ اور ڈیکلیریشن ایمیشن کے درمیان جو تعلق ہے اس کا مطلب یہ ہے کہ آپ کو کراس فائل تجزیہ (cross-file analysis) کی پوری قیمت چکانی پڑتی ہے، چاہے آپ صرف پبلک سطح کی ٹائپس کو ڈسک پر لکھنا چاہتے ہوں۔
isolatedDeclarations قواعد کو کیسے بدلتا ہے
isolatedDeclarations اس تعلق کو توڑ دیتا ہے۔ جب یہ فلیگ فعال ہوتا ہے، تو کمپائلر کسی دوسری فائل سے یہ پوچھے بغیر کہ کسی چیز کا مطلب کیا ہے، ایک سورس فائل کے لیے .d.ts فائل جاری کرنے پر راضی ہو جاتا ہے۔ یہ ایک سادہ معاہدے کی ضرورت کے ذریعے ایسا کرتا ہے: ہر ایکسپورٹ شدہ سمبل (symbol) کے لیے اس جگہ پر ایک واضح اور نظر آنے والا ٹائپ اینوٹیشن (type annotation) ہونا ضروری ہے جہاں اسے ڈکلیئر کیا گیا ہو۔
اگر کمپائلر سورس میں ہی مکمل ٹائپ دیکھ سکتا ہے، تو اسے انفرنس (inference) کرنے کی ضرورت نہیں پڑتی۔ اسے امپورٹس کا پیچھا کرنے کی ضرورت نہیں ہوتی۔ اسے یہ جاننے کی ضرورت نہیں ہوتی کہ کسی دوسری فائل میں User آئیڈینٹیفائر ایک انٹرفیس ہے، ٹائپ ایلس (type alias) ہے، یا کلاس ہے۔ یہ صرف وہی جاری کرتا ہے جو آپ نے لکھا ہے۔
اس کا مطلب ہے کہ فائل A اور فائل B اپنی ڈیکلیریشنز بیک وقت تیار کر سکتی ہیں۔ ایک بلڈ آرکیسٹریٹر (build orchestrator) ہر فائل کو ایک الگ تھریڈ (thread) کے سپرد کر سکتا ہے۔ تیز رفتار ٹرانسپائلرز (transpilers) جو پہلے مکمل ٹائپ چیکر نہ ہونے کی وجہ سے .d.ts جنریشن کو چھوڑ دیتے تھے، اب ڈیکلیریشن فائلیں بھی تیار کر سکتے ہیں، کیونکہ یہ کام خالصتاً سنٹیکٹک (syntactic) ہو جاتا ہے۔
سمجھوتہ: اسے لکھ کر بتائیں (The Tradeoff: Write It Down)
رفتار مفت میں نہیں ملتی۔ آپ کو کسی بھی ایسی چیز کے لیے ٹائپ انفرنس (type inference) پر انحصار کرنا چھوڑنا ہوگا۔ ہر پبلک فنکشن، کلاس، ویری ایبل اور کانسٹنٹ کے لیے اس کی ٹائپ واضح طور پر لکھی ہونی چاہیے۔ اگر TypeScript کو ریٹرن سٹیٹمنٹ کو دیکھ کر یا کسی جینک ایرگومنٹ (generic argument) کو حل کر کے ٹائپ کا حساب لگانا پڑے، تو isolatedDeclarations ایرر دے گا۔
عملی طور پر یہ کچھ ایسا نظر آتا ہے۔ فلیگ کے بغیر، آپ شاید یوں لکھیں:
export function fetchUser(id: number) {
return fetch(`/users/${id}`).then(r => r.json());
}
TypeScript fetch کا معائنہ کر کے، پھر Promise.prototype.then کا، اور پھر r.json() کو ریٹرن کرنے والے گمنام فنکشن کا معائنہ کر کے ریٹرن ٹائپ کا اندازہ لگاتا ہے۔ .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) کو ان کے عناصر سے انفرنس ہونے کے بجائے واضح ٹائپس کی ضرورت ہوتی ہے۔ ایکسپورٹ شدہ آبجیکٹس کو واضح ٹائپ اینوٹیشنز کی ضرورت ہوتی ہے اگر ان کی شکل صارفین (consumers) کے لیے اہم ہو۔ جینک فنکشنز (generic functions) کے لیے ان کے ریٹرن ٹائپس اور کنسٹرینٹس (constraints) ڈکلیریشن سائٹ پر نظر آنے چاہئیں۔ آپ کسی پیچیدہ میپڈ ٹائپ (mapped type) کے نتیجے کو ایکسپورٹ نہیں کر سکتے جب تک کہ آپ اسے ایک نامزد ٹائپ ایلس (type alias) نہ دے دیں جو مکمل طور پر لکھا گیا ہو۔
اس کا فائدہ یہ ہے کہ آپ کا پبلک API خود بخود دستاویزی (self-documenting) بن جاتا ہے۔ صارفین—اور کمپائلر—کو اب امپلیمنٹیشن کی تفصیلات سے آپ کے ارادے کا الٹا تجزیہ (reverse-engineer) کرنے کی ضرورت نہیں پڑتی۔ ٹائپس ایک دانستہ معاہدہ ہیں۔
وقت کہاں بچتا ہے (Where the Time Goes)
ایک بڑے کوڈ بیس میں، اس کا اثر فوری ہوتا ہے۔ بلڈ ٹائم جو منٹوں تک کھنچ جاتا ہے وہ سیکنڈوں میں گر سکتا ہے کیونکہ ڈیکلیریشن ایمیشن اب سب سے بڑی رکاوٹ نہیں رہتی۔ ہر فائل آزادانہ طور پر جاری ہوتی ہے، اس لیے یہ عمل آپ کے پاس موجود کورز (cores) کی تعداد کے ساتھ بڑھتا ہے، نہ کہ آپ کے امپورٹ گراف کی گہرائی کے ساتھ۔
اس سے یہ بھی بدل جاتا ہے کہ آپ کون سے ٹولز استعمال کر سکتے ہیں۔ esbuild اور swc جیسے transpilers پہلے ہی TypeScript کو JavaScript میں تبدیل کرنے میں بجلی کی طرح تیز ہیں، لیکن بہت سی ٹیمیں اب بھی صرف .d.ts فائلیں بنانے کے لیے الگ سے tsc چلاتی ہیں۔ isolatedDeclarations کے ساتھ، وہ تیز رفتار ٹولز دونوں کام سنبھال سکتے ہیں۔ انہیں ڈیکلیریشنز (declarations) تیار کرنے کے لیے TypeScript کے پورے type system کی نقل کرنے کی ضرورت نہیں ہے؛ انہیں صرف syntax کو parse کرنے اور آپ کے فراہم کردہ explicit types کو کاپی کرنے کی ضرورت ہوتی ہے۔ یہ متبادِل toolchains کے ساتھ end-to-end TypeScript builds کو کہیں زیادہ قابل عمل بناتا ہے۔
Distributed اور incremental builds بھی سادہ ہو جاتے ہیں۔ Continuous integration میں، ایک remote cache یا sharded build کسی پیکیج کے لیے اس کے مکمل transitive dependency graph کو پہلے ڈاؤن لوڈ کیے بغیر ڈیکلیریشنز جاری کر سکتا ہے۔ اگر سورس میں types واضح ہوں، تو build shard کے پاس وہ سب کچھ ہوتا ہے جس کی اسے ضرورت ہے۔
کیا چیز وہی رہتی ہے
یہ پابندی صرف exports پر لاگو ہوتی ہے۔ ایک module کے اندر، کام معمول کے مطابق جاری رہتا ہے۔ Local variables، private class members، اور غیر exported helper functions اب بھی مکمل type inference پر انحصار کر سکتے ہیں۔ TypeScript بغیر کسی شکایت کے ایک loop variable یا closure parameter کی type کا اندازہ لگا لے گا۔
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);
}
صرف exported function signature کو annotation کی ضرورت تھی۔ اندرونی میکانزم لچکدار اور اظہار کرنے والا رہتا ہے۔ یہ لکھنے کے بوجھ کو قابلِ برداشت رکھتا ہے۔ آپ ہر جگہ مکمل طور پر explicit style پر منتقل نہیں ہو رہے ہیں؛ آپ صرف ہر module کی boundary پر معاہدے کو رسمی بنا رہے ہیں۔
کیا یہ آپ کے codebase کے لیے درست ہے؟
isolatedDeclarations کو اپنانے سے آپ کے وقت کے استعمال کا طریقہ بدل جاتا ہے۔ جب آپ ایک export لکھتے ہیں تو آپ چند اضافی keystrokes لگاتے ہیں، اور بدلے میں آپ ہر build پر اضافی وقت کا نقصان اٹھانا بند کر دیتے ہیں۔ Library authors کے لیے، یہ اکثر ایک آسان فیصلہ ہوتا ہے۔ Public APIs کو ویسے بھی annotated ہونا چاہیے۔ ایک بند monorepo کے اندر کام کرنے والے application developers کے لیے، ابتدائی لاگت غیر ضروری رسمی کارروائی محسوس ہو سکتی ہے۔ لیکن اگر آپ کی ٹیم build time کو کافی بریک (coffee breaks) کے پیمانے سے ناپتی ہے، تو یہ سودا جلد ہی پرکشش ہو جاتا ہے۔
آپ اسے بتدریج اپنا سکتے ہیں۔ Flag کو فعال کریں، compiler چلائیں، اور exported symbols پر آنے والی غلطیوں کو درست کریں۔ Error messages آپ کو بالکل بتاتے ہیں کہ کون سی public-facing types implicit ہیں۔ انہیں درست کریں، اندرونی چیزوں کو ویسے ہی رہنے دیں، اور اپنے declaration مرحلے کو تیز ہوتے ہوئے دیکھیں۔
ایک بات یاد رکھیں: یہ flag TypeScript کے type checker کو خود تیز نہیں بناتا۔ اگر آپ اپنے editor میں تیز تر فیڈ بیک یا تیز تر tsc --noEmit رن چاہتے ہیں، تو آپ کو اب بھی project references، سخت file inclusion، یا دیگر architectural اصلاحات کی ضرورت ہوگی۔ isolatedDeclarations خاص طور پر .d.ts فائلوں کے emission کو نشانہ بناتا ہے۔ یہ ایک build optimization ہے، type-checking optimization نہیں ہے۔
اصل نچوڑ
isolatedDeclarations آپ سے کہتا ہے کہ آپ اپنی public types کو first-class artifacts کے طور پر سمجھیں۔ Compiler کو انہیں اخذ (deduce) کرنے کے لیے مجبور کرنا بند کریں۔ انہیں خود لکھیں۔ ایک بار جب آپ ایسا کر لیتے ہیں، تو compiler ہر بار ڈیکلیریشن فائل تیار کرنے کے لیے آپ کے پورے dependency graph میں کھوجنا بند کر دیتا ہے۔ یہ parallel طور پر کام کرتا ہے، esbuild اور swc جیسے ٹولز مکمل TypeScript workflows کو سنبھالتے ہیں، اور آپ کے monorepo builds کی رفتار نہیں ڈگمگاتی۔
لاگت build time سے ہٹ کر authoring time پر منتقل ہو جاتی ہے۔ زیادہ تر بڑھتی ہوئی ٹیموں کے لیے، یہ ایک فائدہ مند سودا ہے۔
