تقوم بكتابة نوع (type) يتنقل عبر الكائنات المتداخلة، ويبني مسارات مفصولة بنقاط من أجل الإكمال التلقائي. يعمل بشكل رائع على كائن اختبار صغير. ثم توجهه نحو حمولة API حقيقية، فيتجمد المحرر. في النهاية، يخرج TypeScript بالخطأ TS2589: Type instantiation is excessively deep and possibly infinite.
هذه الرسالة لا تعني أن الكود الخاص بك يحتوي على حلقة لانهائية بالمعنى التقليدي. بل تعني أن المترجم (compiler) قد استسلم. النوع الذي طلبت منه حسابه كان إما غير محدود حقاً، أو محدوداً ولكن كبيراً جداً لدرجة أن تقييمه سيستنفد حدود TypeScript الداخلية. عندما يحدث هذا، يتوقف المترجم قبل أن يتسبب في تعليق بيئة التطوير (IDE) الخاصة بك.
متى يظهر الخطأ TS2589
الأنواع المتكررة (Recursive types) هي المتهم الأكثر شيوعاً. يقوم TypeScript بتقييم الأنواع بنهم، وإذا استمر نوع مساعد في استدعاء نفسه — خاصة من خلال المنطق الشرطي — فإن مكدس الحسابات (computation stack) ينمو بسرعة. ستصطدم عادةً بهذا الجدار في سيناريوهات محددة:
- الأنواع الشرطية المتكررة (Recursive conditional types) التي تقوم بتفكيك
tupleأوobjectأوstring templateبشكل متكرر حتى الوصول إلى الحالة الأساسية. - مولدات مسارات الكائنات المتداخلة بعمق (Deeply nested object path generators)، والتي تحول هياكل مثل
{ user: { address: { street: string } } }إلى اتحادات (unions) من الثوابت النصية (string literals) مثل"user" | "user.address" | "user.address.street". - أنواع القوالب النصية (Template literal types) التي تقوم بتحليل النصوص حرفاً بحرف أو رمزاً برمز.
- الأنواع المخططة (Mapped types) التي يتم تكرارها عبر كائنات تحتوي على عشرات المفاتيح ومستويات متعددة.
- الأنواع الشرطية (Conditional types) التي تتوزع عبر اتحادات كبيرة، مما يضاعف عبء العمل بصمت عبر كل عضو.
مثال المسار المتداخل مغرٍ بشكل خاص. تحب مكتبات النماذج (Form libraries) وأدوات إدارة الحالة (state-management tools) تقديم مسارات محددة النوع (typed paths) لكي تحصل على إكمال تلقائي لأسماء الحقول. في الكائن الضحل، يكون إنشاء كل مسار نقطي قانوني كـ string union أمراً بسيطاً. أما في الكائن العميق أو الواسع، فإن هذا الاتحاد ينفجر. يجب على TypeScript الاحتفاظ بكل تبديلة (permutation) في ذاكرة العمل في وقت واحد. وعند عمق معين، يلاحظ المترجم أن العمل يتجاوز ميزانيته فيقوم بسحب مكابح الطوارئ.
الحل الأول: إضافة حد أقصى للعمق
الطريقة الأكثر مباشرة لحل TS2589 هي التوقف عن التظاهر بأن النوع الخاص بك يمكنه التكرار إلى الأبد. قم بإدخال عداد للعمق يعمل كقاطع دائرة (circuit breaker).
من الناحية العملية، يعني هذا إضافة معامل عام رقمي (numeric generic parameter) — غالباً ما يتم تمثيله كـ tuple يتناقص طوله — ينقص في كل مرة يتكرر فيها النوع. عندما يصل العداد إلى الصفر، يعيد النوع قيمة بديلة واسعة مثل string بدلاً من التعمق أكثر. سيظل المستخدمون يحصلون على إكمال تلقائي دقيق للمستويات الأربعة أو الخمسة الأولى، مما يغطي الغالبية العظمى من الكائنات في العالم الحقيقي. بعد ذلك، يقوم المترجم ببساطة بتوسيع النوع والمضي قدماً.
هذا النهج لا يجعل نوعك المساعد أقل صحة بأي طريقة ذات معنى. بل يجعله محدوداً. نظام الأنواع الذي يتسبب في تعطل المترجم ليس أكثر فائدة من نظام يتنازل بلباقة بعد عمق معقول.
الحل الثاني: التحقق من مسار واحد في كل مرة
إذا كان إنشاء كل مسار ممكن مسبقاً مكلفاً للغاية، فقم بتغيير العقد (contract). بدلاً من إنتاج اتحاد ضخم لجميع النصوص الصالحة، اكتب نوعاً يتحقق مما إذا كانت سلسلة نصية واحدة محددة هي مسار صالح.
فكر في الفرق بين إنشاء قاموس لكل كلمة إنجليزية مقابل التحقق مما إذا كانت كلمة واحدة مكتوبة بشكل صحيح. الأول هو هيكل بيانات ضخم؛ والآخر هو فحص خفيف الوزن. بمصطلحات TypeScript، بدلاً من تصدير أداة Paths<T> التي تنتج "user.address.street" | "user.settings.theme" | ... ، قم بتصدير شيء مثل IsValidPath<T, "user.address.street">. يقوم المترجم فقط بتقييم المسار الذي تمرره بالفعل.
يغير هذا التحول طريقة تصميم واجهات برمجة التطبيقات (APIs). قد تقبل تواقيع الدوال (function signatures) الخاصة بك سلسلة نصية ثم تستخدم قيداً عاماً (generic constraint) للتحقق منها مقابل شكل الكائن. سيظل المحرر يشتكي إذا كتب المطور مساراً خاطئاً، لكن المترجم لن يضطر أبداً إلى تجسيد (materialize) المجموعة الكاملة من المسارات القانونية أثناء فحص النوع. بالنسبة للكائنات الكبيرة، يكون الفرق في الأداء هائلاً.
تكتيكات سريعة تبقيك مستمراً
بعيداً عن الإصلاحين الهيكليين، يمكن لبعض العادات الأصغر أن تمنع الأنواع المتكررة من تجاوز الحد:
قم بتغليف معاملات النوع (type parameters) داخل tuples لمنع التوزيع. إن وجود معامل نوع مجرد في شرط، مثل
T extends Foo ? Bar : Bazيؤدي إلى توزيع التحقق على كل عضو عندما يكونTعبارة عن union. إذا كان هذا الـ union يحتوي على خمسين عضواً، فسيقوم TypeScript بخمسين عملية instantiation منفصلة. أما كتابة[T] extends [Foo] ? Bar : Bazفتقوم بتقييم الشرط مرة واحدة مقابل الـ union بالكامل. استخدم هذا الأسلوب كلما لم تكن بحاجة فعلياً إلى مطابقة النوع مع كل عضو في الـ union بشكل فردي.قم بتقليص مدخلاتك أثناء تصحيح الأخطاء. عندما تظهر رسالة الخطأ TS2589، استبدل نوع كائن الإنتاج (production object type) بنموذج مبسط (stub) صغير يحتوي على خاصيتين ومستوى واحد من التداخل (nesting). إذا اختفى الخطأ، فقد تأكدت من أن المشكلة تكمن في العمق أو عدد العناصر (cardinality)، وليس في خطأ برمجي. هذا يجنبك إعادة كتابة منطق كان سليماً من الناحية الهيكلية.
خفف من صرامة أنواع واجهة برمجة التطبيقات (API) الموجهة للجمهور. داخلياً، قد تحتاج إلى دقة متناهية، ولكن خارجياً، قد تكون المثالية أحياناً مكلفة أكثر مما تستحق. إذا كان استخدام نوع إكمال تلقائي (autocomplete) أوسع قليلاً يمنع حدوث تأخير لمدة ثانيتين في المحرر، فإن هذه المقايضة تستحق العناء عادةً. يمكنك دمج النوع الأكثر مرونة مع متحقق وقت تشغيل (runtime validator) لالتقاط المسارات الخاطئة أثناء الاختبار.
لماذا يفرض TypeScript هذا الحد
لا يمكن لـ TypeScript حل "مشكلة التوقف" (halting problem). فهو لا يعرف ما إذا كان النوع التكراري (recursive type) الخاص بك سينتهي في النهاية أم سيستمر في الدوران إلى الأبد. وبدلاً من المخاطرة بوقوع حلقة مفرغة (infinite loop) داخل المترجم (compiler)، فإنه يفرض حداً تحفظياً. أحياناً، يوقف هذا الحد نوعاً كان سوف ينتهي لو مُنح وقتاً كافياً. رسالة TS2589 هي اعتراف من المترجم بأنه يفضل الأمان على الندم.
إن احترام هذا الحد هو جزء من كتابة أنواع برمجية بمستوى الإنتاج (production-grade types). فتعريف النوع هو كود يعمل داخل المترجم، والكود المكلف له عواقب حقيقية. فبطء الإكمال التلقائي يضر بسرعة المطورين تماماً كما يضر بطء كود وقت التشغيل (runtime code) بتجربة المستخدم.
الخلاصة الحقيقية
ليست رسالة TS2589 إشارة إلى أنك مبرمج سيئ لأنظمة الأنواع. بل هي إشارة إلى أن نوعك يقوم بالكثير من العمل في وقت واحد. ضع حداً للتكرار (recursion)، وتحقق من البيانات بأسلوب كسول (lazily)، واحذر من التوزيع غير الضروري. الهدف من الأنواع المتقدمة ليس إثبات كل حقيقة ممكنة وقت التجميع (compile time)؛ بل هو تزويد فريقك بأدوات سريعة وموثوقة. فالنوع الذي يتم تجميعه في أجزاء من الثانية ويغطي 95% من الحالات أكثر قيمة بكثير من نوع مثالي نظرياً ولكنه يتسبب في تعطل خادم اللغة (language server).
