لماذا هذا الموضوع يحظى باهتمام كبير على Stack Overflow؟
\n\nإذا كنت قد كتبت كودًا بلغة TypeScript وحاولت إنشاء كائن حيث جميع القيم يجب أن تكون من نوع واحد، فمن المحتمل أنك واجهت هذه المشكلة. سؤال "Enforcing the type of the indexed members of a Typescript object" حصل على آلاف المشاهدات على Stack Overflow ليس من قبيل الصدفة - إنها مشكلة كلاسيكية للمطورين الذين ينتقلون من JavaScript.
\n\nتخيل: لديك قاموس تخزين مؤقت، أو إعدادات بمفاتيح ديناميكية، أو مجرد خريطة بأرقام. تريد أن يتأكد TypeScript من أن كل قيمة في الكائن هي نص، أو رقم، أو نوع معقد. لكن المترجم يسمح بمرور الأخطاء. هل هذا مألوف؟
\n\nفي هذه المقالة، سنستعرض جميع الطرق الرئيسية لحل هذه المشكلة: من Index Signatures البسيطة إلى Mapped Types المتقدمة. ستتعرف متى تستخدم كل طريقة وما هي المخاطر التي تنتظرك.
المشكلة: كائن بمفاتيح ديناميكية
\n\nلنفترض أننا نكتب نظامًا لإدارة المستخدمين. لدينا كائن حيث المفاتيح هي معرفات المستخدمين، والقيم هي أسمائهم:
\n\ninterface UserCache {\n [userId: string]: string;\n}\n\nconst cache: UserCache = {\n 'user_1': 'أحمد',\n 'user_2': 'سارة',\n // 'user_3': 123; // خطأ! يجب أن يكون نصًا\n};\n\ncache['user_4'] = 'خالد'; // صحيح\ncache['user_5'] = 42; // خطأ! النوع 'number' غير قابل للتخصيص لـ 'string'\n\n\nيبدو أن كل شيء يعمل. لكن هناك تفصيل: Index Signature يسمح بأي مفاتيح نصية. ماذا لو أردنا تقييد مجموعة المفاتيح المسموح بها؟
الطريقة 1: Index Signature - الطريقة الكلاسيكية
\n\nIndex Signature هي أبسط طريقة لتحديد نوع جميع قيم الكائن. الصيغة:
\n\ninterface StringMap {\n [key: string]: string;\n}\n\nconst map: StringMap = {\n hello: 'العالم',\n foo: 'bar',\n};\n\n\nالمزايا:
\n- \n
- البساطة وسهولة القراءة \n
- تعمل مع أي عدد من المفاتيح \n
العيوب:
\n- \n
- لا يمكن تقييد مجموعة المفاتيح \n
- جميع القيم يجب أن تكون من نوع واحد (أو union) \n
- لا تعمل مع قيم
undefined(يجب الحذر) \n
خطأ شائع للمبتدئين: محاولة إضافة دالة إلى هذا الكائن:
\n\ninterface BadMap {\n [key: string]: string;\n length: number; // خطأ! 'number' غير قابل للتخصيص لـ 'string'\n}\n\n\nهذا لن يعمل لأن length يخالف النوع العام. إذا كنت بحاجة لخلط الأنواع، استخدم union أو Mapped Types.
الطريقة 2: Record - المساعد المدمج
\n\nيوفر TypeScript نوعًا مدمجًا Record<K, V> يفعل نفس الشيء ولكن بصيغة أكثر إيجازًا:
type StringRecord = Record<string, string>;\n\nconst config: StringRecord = {\n apiUrl: 'https://api.example.com',\n timeout: '5000', // صحيح، نص\n // port: 8080, // خطأ!\n};\n\n// يمكن تقييد المفاتيح:\ntype AllowedKeys = 'name' | 'email' | 'phone';\ntype UserRecord = Record<AllowedKeys, string>;\n\nconst user: UserRecord = {\n name: 'أحمد',\n email: 'ahmed@example.com',\n phone: '+1234567890',\n // age: '30', // خطأ! 'age' ليس ضمن AllowedKeys\n};\n\n\nمتى تستخدم:
\n- \n
- عند الحاجة لإنشاء قاموس مقيد الأنواع بسرعة \n
- عندما تكون المفاتيح مجموعة محددة من القيم (union) \n
- للإعدادات ذات الحقول المعروفة \n
العيب: Record لا يسمح بتحديد أنواع مختلفة لمفاتيح مختلفة. لهذا يوجد Mapped Types.
الطريقة 3: Map - البديل من ES6
\n\nإذا كنت تستخدم Map من JavaScript، فإن TypeScript يقوم بتحديد أنواعه بشكل ممتاز