مقدمة: لماذا يعترض TypeScript على الكود الخاص بك؟
\n\nهل سبق لك أن كتبت كودًا بلغة TypeScript يبدو منطقيًا تمامًا، ولكن المترجم فجأة يظهر خطأً مخيفًا؟ إذا واجهت الرسالة «An index signature parameter type cannot be a union type. Consider using a mapped object type instead» — فأنت لست وحدك. هذا الخطأ هو أحد أكثر الأسئلة شيوعًا على Stack Overflow (آلاف المشاهدات!)، وغالبًا ما يحير حتى المطورين ذوي الخبرة.
\n\nفي هذه المقالة، لن نعرض فقط كيفية إصلاح الخطأ. سنشرح لماذا TypeScript صارم جدًا، وكيفية استخدام mapped object types لجعل الكود الخاص بك ليس فقط قابلاً للعمل، بل وأكثر أمانًا. هيا بنا!
\n\nما هو index signature وما الغرض منه؟
\n\nتخيل أنك تنشئ كائنًا، حيث المفاتيح هي نصوص والقيم هي أرقام. على سبيل المثال، قاموس بأسعار المنتجات:
\n\n// كائن عادي\nconst prices = {\n apple: 100,\n banana: 50,\n cherry: 200\n};\n\nولكن ماذا لو أردت أن تقول لـ TypeScript: «اسمع، هذا الكائن يمكن أن يحتوي على أي مفاتيح نصية، وكل القيم ستكون أرقامًا»؟ لهذا نحتاج إلى index signature — بناء جملة خاص يصف نمطًا لجميع خصائص الكائن:
\n\n// Index signature: [key: string]: number\ninterface PriceMap {\n [key: string]: number;\n}\n\nconst prices: PriceMap = {\n apple: 100,\n banana: 50\n};\n\nهنا [key: string]: number هو index signature. وهو يقول: «المفاتيح نصوص، والقيم أرقام». الأمر بسيط، أليس كذلك؟
ما المشكلة مع union-أنواع؟
\n\nالآن تخيل أنك تريد تقييد المفاتيح المحتملة إلى خيارين فقط: 'name' أو 'age'. يبدو من المنطقي كتابة ما يلي:
// ❌ خطأ! لا يمكن فعل ذلك\ninterface Person {\n [key: 'name' | 'age']: string | number;\n}\n\nوهنا يظهر TypeScript الخطأ نفسه: «An index signature parameter type cannot be a union type. Consider using a mapped object type instead».
\n\nلماذا؟ لأن index signature مصمم لوصف مجموعة غير محدودة من المفاتيح. إذا كنت تريد تحديد قائمة محددة من المفاتيح، فهذا لم يعد نمطًا، بل وصفًا دقيقًا للهيكل. TypeScript لا يسمح بخلط هذه المفاهيم.
\n\nكيفية إصلاح الخطأ؟ استخدام Mapped Object Type
\n\nالحل الذي يقترحه المترجم نفسه هو استخدام mapped object type (نوع الكائن المُعيّن). يبدو الأمر معقدًا، لكنه في الواقع مجرد طريقة «للتكرار» على جميع المفاتيح من union type وإنشاء خاصية لكل منها.
\n\nإليك كيف يبدو ذلك:
\n\n// ✅ صحيح: استخدام mapped type\ntype Person = {\n [K in 'name' | 'age']: K extends 'name' ? string : number;\n};\n\n// أو خيار أبسط إذا كانت أنواع القيم متطابقة:\ntype PersonSimple = {\n [K in 'name' | 'age']: string | number;\n};\n\nدعنا نحلل بناء الجملة [K in 'name' | 'age']:
- \n
K— متغير يأخذ بدوره كل قيمة من union type ('name'، ثم'age'). \n in— كلمة مفتاحية تبدأ «حلقة» عبر الأنواع. \n 'name' | 'age'— مجموعة المفاتيح لدينا. \n
ونتيجة لذلك، سينشئ TypeScript كائنًا بخاصيتين محددتين: name و age. اختفى الخطأ!
مثال مع أنواع قيم مختلفة
\n\nغالبًا ما يحدث أن المفاتيح المختلفة يجب أن يكون لها أنواع قيم مختلفة. باستخدام الأنواع الشرطية (conditional types) يتم حل ذلك بأناقة:
\n\ntype Config = {\n [K in 'host' | 'port' | 'debug']: \n K extends 'host' ? string :\n K extends 'port' ? number :\n boolean;\n};\n\n// الآن:\n