مقدمة: لماذا إضافة خاصية إلى window تمثل صداعًا لكل مطور TypeScript؟
تخيل الموقف: أنت تكتب تطبيق SPA حديثًا باستخدام TypeScript. كل شيء يسير بسلاسة حتى تظهر الحاجة لحفظ حالة عامة أو التكامل مع سكريبت خارجي (مثل التحليلات أو الخرائط أو بوابة الدفع). تكتب:
window.myAppConfig = { apiKey: '123' };وهنا يصدر TypeScript خطأ أحمر: "Property 'myAppConfig' does not exist on type 'Window & typeof globalThis'". هل هذا مألوف؟ هذا السؤال حصد آلاف المشاهدات على Stack Overflow، واليوم لن نقدم حلاً جاهزًا فحسب، بل سننظر تحت غطاء TypeScript لنفهم لماذا يحدث ذلك وكيف نتعامل معه بأناقة وأمان.
في هذه المقالة، سنستعرض عدة طرق - من "الحلول المؤقتة" للمبتدئين إلى الأنماط الاحترافية التي يستخدمها كبار المطورين. هيا بنا!
تحت الغطاء: لماذا يشتكي TypeScript من window.myProp؟
لفهم المشكلة، يجب أن نتذكر أن TypeScript هو امتداد لـ JavaScript يضيف التحقق الثابت من الأنواع. كائن window في المتصفح هو كائن عام له نوع محدد مسبقًا (Window). هذا النوع موصوف في المكتبات القياسية لـ TypeScript (lib.dom.d.ts) ويتضمن جميع الخصائص القياسية: document و location و setTimeout ومئات غيرها.
عند محاولة تعيين window.myProp، يتحقق TypeScript: "هل توجد هذه الخاصية في واجهة Window؟". لا - إذن خطأ. هذه حماية من الأخطاء الإملائية والتلوث العرضي للنطاق العام. لكن ماذا تفعل إذا كنت تتعمد توسيع window؟
الطريقة 1: الأسهل ولكن الأكثر خطورة - as any
الخيار الأول الذي يتبادر إلى ذهن المبتدئ هو ببساطة "إسكات" TypeScript باستخدام تحويل النوع:
(window as any).myAppConfig = { apiKey: '123' };الإيجابيات: سريع، يعمل، لا يتطلب تعريفات إضافية.
السلبيات: تفقد جميع مزايا TypeScript. لا إكمال تلقائي، لا تحقق من الأنواع. إذا كتبت بالخطأ myAppConfigg - لن يظهر خطأ، لكن التطبيق سيتعطل في وقت التشغيل. هذا يشبه إطفاء الوسائد الهوائية في السيارة - يمكنك القيادة ولكن الأمر محفوف بالمخاطر.
متى تستخدم: فقط للنمذجة السريعة أو إذا كنت متأكدًا من أن الخاصية ستستخدم مرة واحدة وستُحذف.
الطريقة 2: توسيع الواجهة العامة (Interface Merging)
يدعم TypeScript دمج الواجهات (declaration merging). هذه آلية رئيسية تسمح "بإضافة" خصائص جديدة إلى النوع الموجود Window. للقيام بذلك، تحتاج إلى تعريف واجهة بنفس الاسم في النطاق العام.
أنشئ ملفًا global.d.ts (أو أضف إلى أي ملف .d.ts في مشروعك):
// global.d.tsinterface Window { myAppConfig: { apiKey: string; debugMode?: boolean; }; myAnalyticsFunction: (event: string, data: object) => void;}الآن يمكنك الكتابة:
window.myAppConfig = { apiKey: '123' };window.myAnalyticsFunction('page_view', { page: '/home' });
// سيقترح TypeScript أن apiKey من نوع stringconsole.log(window.myAppConfig.apiKey); // ✅ بدون أخطاءكيف يعمل هذا تحت الغطاء؟ يقوم TypeScript تلقائيًا بدمج جميع تعريفات الواجهات التي تحمل نفس الاسم. إذا كان هناك بالفعل interface Window { ... } في lib.dom.d.ts، فإن واجهتك التي تحمل نفس الاسم ستضيف ببساطة خصائص جديدة إليها. وهذا ما يسمى glob