تخطّى إلى المحتوى الرئيسي
لماذا يصبح فحص أنواع TypeScript بطيئًا: ملاحظاتنا الميدانية حول tsgo ومراجع المشاريع وisolatedDeclarations

لماذا يصبح فحص أنواع TypeScript بطيئًا: ملاحظاتنا الميدانية حول tsgo ومراجع المشاريع وisolatedDeclarations

September 7, 2026
أدوات المطورين
8 دقيقة قراءة

فحص الأنواع البطيء عبر tsc --noEmit لا ينتج تقريبًا أبدًا عن عدد الملفات في المستودع، بل عن حجم إنشاء الأنواع (type instantiation) الذي يضطر المدقق إلى تنفيذه، والخيار ‎--extendedDiagnostics‎ يخبرك بالسبب خلال عشر ثوانٍ تقريبًا. التغييرات الثلاثة التي خفّضت زمن فحص الأنواع في مشاريعنا هي: القياس قبل التخمين، وتقسيم ملف tsconfig.json الواحد الذي يغطي المستودع كله إلى مراجع مشاريع تُبنى عبر tsc -b، وتشغيل المنفذ الأصلي المكتوب بلغة Go واسمه tsgo كمدقق ثانٍ في التكامل المستمر.

أهم النقاط

تشغيل tsc --noEmit --extendedDiagnostics يطبع عدد الملفات والأنواع وInstantiations والذاكرة المستخدمة، إضافة إلى توزيع الزمن على مراحل Parse وBind وCheck وEmit. نقرأ هذا الخرج قبل تغيير أي إعداد، لأن علاج بطء مرحلة Parse لا يمت بصلة لعلاج بطء مرحلة Check.

عدد Instantiations — أي عدد المرات التي ينشئ فيها المدقق نوعًا عامًا بوسائط محددة — يتنبأ بتكلفة الفحص أفضل بكثير من عدد الملفات. مشروع بألفي ملف وملايين من الـinstantiations يُفحص أبطأ من مشروع بثمانية آلاف ملف وعدد قليل منها.

tsgo هو المنفذ الأصلي لـTypeScript: إعادة كتابة للمترجم وخدمة اللغة بلغة Go، تُوزَّع عبر حزمة ‎@typescript/native-preview‎ ومن المخطط أن تُطرح باعتبارها TypeScript 7. وهو الرافعة الوحيدة في هذه القائمة التي تُسرّع الفحص دون تغيير سطر واحد من كود التطبيق.

مراجع المشاريع — أي composite: true في كل حزمة إضافة إلى مصفوفة references في الحزمة المستهلكة، مع البناء عبر tsc -b — تحوّل فحصًا واحدًا للمستودع كله إلى رسم بياني من الفحوص القابلة للتخزين المؤقت بشكل مستقل. ولا تؤتي ثمارها إلا إذا كان رسم الاعتماديات مقسّمًا إلى طبقات فعلية.

الخيار isolatedDeclarations، الذي أُضيف في TypeScript 5.5، يشترط تعليقات نوعية صريحة على القيم المُصدَّرة، ويمنح في المقابل إمكانية اشتقاق ملف .d.ts من ملف مصدري واحد فقط. هذه الخاصية هي ما يسمح بتوليد ملفات التصريح على التوازي أو بإسناده إلى أداة خارج TypeScript.

أين يقضي tsc وقته فعليًا؟

يقسّم tsc عمله إلى أربع مراحل قابلة للقياس — Parse وBind وCheck وEmit — وفي التطبيقات الكبيرة تهيمن مرحلة Check دائمًا تقريبًا. والخيار ‎--extendedDiagnostics‎ يطبع هذا التوزيع مع الأعداد التي تفسّره، ولذلك فهو أول أمر يشغّله فريقنا على أي مشروع بطيء.

حين تهيمن مرحلة Check، تكون الخطوة التالية هي tsc --noEmit --generateTrace .trace ثم تشغيل حزمة ‎@typescript/analyze-trace‎ على المجلد نفسه. يعود التتبّع بقائمة مرتبة بالملفات وعمليات إنشاء الأنواع الأكثر كلفة بالميلي ثانية، والترتيب مفاجئ عادةً: وحدة أدوات واحدة تُصدّر أنواع إرجاع مستنتجة هي متهم متكرر لا تلتقطه أي مراجعة كود.

يستحق التحقق من إعدادين قبل أي عمل بنيوي. ضبط skipLibCheck على true يمنع المترجم من فحص محتويات ملفات .d.ts، وهو غالبًا أرخص مكسب متاح في مستودع بـnode_modules ضخم؛ ويبقى الكود المصدري الخاص بك مفحوصًا مقابل تلك التصريحات. وضبط types على مصفوفة فارغة يمنع TypeScript من تضمين كل حزمة تحت node_modules/@types تلقائيًا، وهو أمر مهم في المستودعات التي تراكمت فيها سنوات من التعريفات العامة التي لا يستوردها أحد.

ما هو tsgo وهل نتبنّاه الآن؟

tsgo هو نقل لمترجم TypeScript وخدمة اللغة من TypeScript إلى Go، أعلنه فريق TypeScript في مارس 2025 ونشره كإصدار معاينة تحت حزمة ‎@typescript/native-preview‎. وقد وصف الفريق خطة يصبح فيها المنفذ الأصلي هو TypeScript 7 بينما تستمر النسخة المكتوبة بجافاسكريبت باسم TypeScript 6، فيتعايش المساران خلال الانتقال. وتشير القياسات التي نشرها الفريق إلى تحسّن بمرتبة عشرية في زمن الفحص وزمن التحميل داخل المحرر على المستودعات التي قاسوها.

لا نتعامل مع tsgo كبديل ما دام يحمل وسم المعاينة، وننصح عملاءنا بالحذر نفسه. وما نفعله بدلًا من ذلك رخيص: نضيف مهمة ثانية غير حاجبة في التكامل المستمر تشغّل tsgo --noEmit إلى جانب المهمة المعتمدة tsc --noEmit، ثم نقارن مخرجات الأخطاء. وحين يتفق الاثنان على كل طلب دمج لمدة شهر، يكون الملف التنفيذي الأصلي قد استحق أن يصبح هو الحاجب.

الحدود الصريحة مهمة. لا تنفّذ المعاينة كل رايات المترجم ولا واجهة المترجم البرمجية العامة بالكامل، لذا فإن كل ما يتصل بـTypeScript برمجيًا — المحوّلات المخصصة، والقواعد المعتمدة على الأنواع في ‎@typescript-eslint‎، وأدوات تعديل الكود المبنية على ts-morph — ما زال يعمل على المترجم المكتوب بجافاسكريبت. وتوجد إضافة معاينة أصلية لمحرر VS Code ينطبق عليها التحفّظ نفسه.

متى تفيد مراجع المشاريع فعلًا؟

تفيد مراجع المشاريع حين يكون المستودع رسمًا بيانيًا مقسّمًا إلى طبقات، ولا تفيد إطلاقًا حين يكون كتلة متشابكة. المشروع من نوع composite يولّد ملفات .d.ts وبصمة .tsbuildinfo، ويمشي tsc -b على رسم المراجع بترتيب الاعتمادية متجاوزًا أي مشروع لم تتغير مدخلاته. فإذا كانت حزمة ui تستورد حزمة core فقط، فلا ينبغي أن يؤدي تعديل ui إلى إعادة فحص core.

ثلاث تفاصيل تحسم ما إذا كان الترحيل يستحق العناء. يجب أن يولّد المشروع المرجعي مخرجات، لأن composite: true يستلزم declaration: true، والمستهلك يفحص الأنواع مقابل ملف .d.ts المولَّد لا مقابل الكود المصدري — وهذه الوساطة هي بالضبط مصدر التخزين المؤقت. وضبط declarationMap على true يبقي أمر الانتقال إلى التعريف يقود إلى ملفات التنفيذ الحقيقية بدل التصريحات المولَّدة. وتشغيل tsc -b --verbose مرة واحدة بعد الإعداد يسمّي كل مشروع يتجاوزه باعتباره محدّثًا، وهو الدليل المباشر الوحيد على أن التخزين المؤقت يعمل بدل إعادة البناء الصامتة.

تستحق حالة الفشل أن تُقال صراحة. في مستودع أحادي تستورد فيه كل حزمة كل الحزم الأخرى، لا توجد طبقات في رسم المراجع، ويُبطل أي تغيير كل المشاريع، ويكون الفريق قد أضاف تعقيد إعدادات دون أي إصابة في الذاكرة المؤقتة. أصلح رسم الاستيرادات أولًا، أو تجاهل المراجع تمامًا.

ماذا يقدّم isolatedDeclarations؟

الخيار isolatedDeclarations راية مترجم أُضيفت في TypeScript 5.5 وتُبلّغ عن خطأ كلما تعذّر إنتاج ملف تصريح من ملف مصدري واحد بمعزل عن غيره — أي عمليًا كلما كان نوع قيمة مُصدَّرة مستنتجًا من وحدة أخرى. وتفعيله تغيير في الكود لا مجرد تغيير في الإعدادات، لأنه يفرض تعليقات صريحة على كل تصدير.

العائد أن توليد التصريحات لم يعد بحاجة إلى فحص أنواع للبرنامج كله، فصار ممكنًا توزيعه على التوازي لكل ملف أو إسناده إلى مولّد أسرع خارج TypeScript؛ وأدوات oxc المكتوبة بلغة Rust تنفّذ هذا بالضبط، وتعرضه الحزم المبنية عليها كوضع لتوليد التصريحات. ويظهر العائد الثانوي في المشروع المستهلك، حيث تصبح دالة مُصدَّرة بنوع إرجاع مكتوب نوعًا يقرؤه المدقق بدل نوع يعيد اشتقاقه.

نفعّله في الحزم التي تنشر تصريحات وفي حزم مساحة العمل الداخلية المستهلكة عبر مراجع المشاريع. ونتركه معطّلًا في كود الميزات الطرفية داخل التطبيق، حيث يكون عبء التعليقات حقيقيًا ولا يوجد توليد تصريحات يُراد تسريعه.

أي أنماط الأنواع مكلفة؟

الأنماط المكلفة هي التي تجعل المدقق يعمل عند كل موضع استدعاء بدل مرة واحدة. الأنواع الشرطية المتداخلة بعمق، وأنواع القوالب النصية العَودية، والاتحادات الكبيرة الموزَّعة عبر نوع مُخطَّط، وسلاسل الاستنتاج الطويلة عبر مكتبات المخططات، كلها تشترك في الشكل نفسه: تُدفع التكلفة مجددًا في كل موضع يُستخدم فيه النوع.

أرخص تصحيح هو تسمية النوع المستنتج مرة واحدة ثم تعليق التصدير به. فكتابة نوع User باعتباره z.infer للمخطط ثم تعليق نوع إرجاع دالة التحليل بأنه User تستبدل بإعادة الاستنتاج في كل موضع استدعاء عملية بحث بسيطة.

عادتان أصغر تحرّكان عدد Instantiations. فضّل واجهة interface تمتد من أنواع أخرى على تقاطع أنواع كائنات كبيرة، لأن الواجهات تُحلّ بكسل وتُخزَّن بالاسم بينما تُعاد حوسبة التقاطعات بنيويًا. وعلّق نوع الإرجاع لكل دالة مُصدَّرة، وهو الانضباط نفسه الذي يفرضه isolatedDeclarations آليًا.

توضيح واحد يوفّر كثيرًا من التصحيح المشوَّش: سرعة الفحص في سطر الأوامر مع بطء المحرر ليسا تناقضًا. فسطر الأوامر يشغّل tsc مرة واحدة على البرنامج كله، بينما يشغّل المحرر tsserver الذي يحتفظ ببرنامج تزايدي حول الملفات المفتوحة ويجيب طلبات الإكمال والتحويم مقابله. فإذا كان سطر الأوامر سريعًا والكتابة بطيئة، فالمشكلة في نطاق مشروع المحرر — افتح سجل TS Server من لوحة الأوامر وتحقق من ملف tsconfig.json الذي أسند إليه ملفك.

أي رافعة نستخدم أولًا؟

ضبط skipLibCheck على true يتخطى فحص الأنواع داخل ملفات .d.ts، ويستحق التفعيل دائمًا تقريبًا لأنه أرخص راية منفردة متاحة.

ضبط incremental على true يعيد استخدام ملف .tsbuildinfo من التشغيل السابق، وهو يفيد حلقات المراقبة المحلية ولا يفيد التكامل المستمر إلا إذا خُزِّن ذلك الملف واستُعيد بين التشغيلات.

تبنّي tsgo يشغّل الفحص نفسه على مترجم Go، وهو الخطوة الأولى الصحيحة حين يكون الهدف السرعة دون تغيير الكود — شغّله بجانب tsc قبل اعتماده مرجعًا.

تبنّي مراجع المشاريع مع tsc -b يضيف تخزينًا مؤقتًا لكل حزمة عبر رسم بياني مقسّم إلى طبقات، ويستحق العناء في مستودع أحادي تشكّل حزمه رسمًا بيانيًا موجّهًا غير دوري فعليًا.

تفعيل isolatedDeclarations يمنح توليد تصريحات لكل ملف قابلًا للتوازي، ويستحق العناء حين ينشر الفريق حزمًا أو حين يهيمن توليد التصريحات على زمن البناء.

إضافة أنواع إرجاع صريحة للدوال المُصدَّرة تزيل الاستنتاج المتكرر عند مواضع الاستدعاء، وهي الرافعة الصحيحة حين يكون عدد Instantiations مرتفعًا مقارنة بعدد الملفات.

الدرس الذي نعود إليه دائمًا: المترجم يُبلّغ أصلًا عن مواضع إنفاق وقته، والفرق تقضي فترات كاملة في ضبط أشياء لم تكن يومًا عنق الزجاجة لأن أحدًا لم يسأله. تشغيل ‎--extendedDiagnostics‎ يكلّف أمرًا واحدًا، ومكانه قبل أي ترحيل.

الأسئلة الشائعة

س: هل يخفي skipLibCheck: true أخطاءً حقيقية في كودنا؟

ج: لا. فهو يتخطى فحص الأنواع داخل ملفات .d.ts فقط، ويبقى كودك المصدري مفحوصًا مقابل تلك التصريحات. والمقايضة أن تعارضًا حقيقيًا بين أنواع مكتبتين لن يُبلَّغ عنه.

س: هل استخدام tsgo كفحص الأنواع الوحيد في التكامل المستمر آمن اليوم؟

ج: ليس ما دام يُطرح باسم ‎@typescript/native-preview‎. شغّله كمهمة ثانية غير حاجبة إلى جانب tsc --noEmit وقارن مخرجات الأخطاء على طلبات دمج حقيقية قبل اعتماده مرجعًا.

س: لماذا يبطؤ المحرر بينما فحص الأنواع في سطر الأوامر سريع؟

ج: يشغّل المحرر tsserver ببرنامج تزايدي خاص به محصور بملف tsconfig.json الذي أسند إليه الملف، وقد يشمل ملفات أكثر بكثير مما تتوقع. افتح سجل TS Server من لوحة الأوامر لترى المشروع المُحدَّد وعدد ملفاته.

س: هل يفيد incremental: true في التكامل المستمر؟

ج: فقط إذا خُزِّن ملف .tsbuildinfo واستُعيد بين التشغيلات. أما على نسخة نظيفة بلا ذاكرة مؤقتة مستعادة فالراية تكلّف قدرًا يسيرًا من زمن الكتابة ولا توفّر شيئًا.

س: هل تسرّع مراجع المشاريع أمر next build؟

ج: هي تسرّع حزم مساحة العمل التي يستهلكها التطبيق، لا فحص التطبيق نفسه. والترتيب الشائع هو ضبط typescript.ignoreBuildErrors في next.config.ts وتشغيل مهمة فحص أنواع مخصصة بالتوازي، فيظل خطأ النوع يُفشل التكامل المستمر دون أن يُسلسل البناء خلف المدقق.