Async/await و Array.map: عندما يصبح أفضل الأصدقاء أعداء
\n\nهل سبق لك أن كتبت شيئًا مثل const results = await array.map(async (item) => {...}) وحصلت على مصفوفة من Promise بدلاً من البيانات المتوقعة؟ إذا كان الأمر كذلك، فأنت لست وحدك. هذا السؤال على Stack Overflow حصد عشرات الآلاف من المشاهدات، وعدد المطورين الذين وقعوا في هذا الفخ يعد بالملايين.
في هذه المقالة، سنقوم بتحليل أخطر 5 أخطاء عند استخدام async/await مع Array.map في TypeScript. والأهم من ذلك، سأوضح لك كيفية تجنبها. استعدوا، سيكون مؤلمًا ولكنه مفيد.
الخطأ رقم 1: نسيان Promise.all (الأكثر شيوعًا)
\n\nالخطأ الأكثر شيوعًا هو الاعتقاد بأن await داخل map سينتظر بشكل سحري جميع الـ Promises. مفاجأة: لا، لن ينتظر.
المشكلة
\n// ❌ هذا لا يعمل\nconst users = [1, 2, 3, 4, 5];\nconst userData = await users.map(async (id) => {\n const response = await fetch(`/api/users/${id}`);\n return response.json();\n});\n// userData هو Promise[]، وليس UserData[]!\nconsole.log(userData); // [Promise, Promise, Promise, Promise, Promise]\n\nلماذا يحدث هذا؟
\nArray.map متزامن (synchronous). لا ينتظر اكتمال الاستدعاءات غير المتزامنة. كل استدعاء لدالة async يُرجع Promise، و map ببساطة يجمعها في مصفوفة. await قبل map ينتظر فقط اكتمال map نفسه، وليس استدعاءاته.
الحل
\n// ✅ صحيح: نلفها في Promise.all\nconst users = [1, 2, 3, 4, 5];\nconst userData = await Promise.all(\n users.map(async (id) => {\n const response = await fetch(`/api/users/${id}`);\n return response.json();\n })\n);\n// userData هو UserData[]! 🎉\nconsole.log(userData);\n\nالخطأ رقم 2: تنفيذ جميع الـ Promises بالتوازي (بينما نحتاج إلى التسلسل)
\n\nPromise.all يشغل جميع الـ Promises بالتوازي. هذا رائع للأداء، لكن في بعض الأحيان نحتاج إلى تنفيذ الطلبات واحدًا تلو الآخر بدقة. على سبيل المثال، عند العمل مع API يحد من عدد الطلبات، أو عندما يعتمد كل طلب تالٍ على الطلب السابق.
المشكلة
\n// ❌ جميع الطلبات تُرسل في نفس الوقت\nconst ids = [1, 2, 3, 4, 5];\nconst results = await Promise.all(\n ids.map(async (id) => {\n // 5 طلبات إلى API في نفس الوقت - قد تحصل على حد معدل\n return await api.processItem(id);\n })\n);\n\nالحل: التنفيذ المتسلسل
\n// ✅ نستخدم for...of مع await\nconst ids = [1, 2, 3, 4, 5];\nconst results: ItemData[] = [];\n\nfor (const id of ids) {\n const result = await api.processItem(id);\n results.push(result);\n}\n// results — مصفوفة بالبيانات، تمت معالجتها بالترتيب\n\nبديل آخر — Array.reduce مع مركم (accumulator) من نوع Promise:
// ✅ يعمل أيضًا، لكنه أقل قابلية للقراءة\nconst ids = [1, 2, 3, 4, 5];\nconst results = await ids.reduce(async (accPromise, id) => {\n const acc = await accPromise;\n const result = await api.processItem(id);\n return [...acc, result];\n}, Promise.resolve([] as ItemData[]));\n\nالخطأ رقم 3: كتابة الأنواع (Typing) بشكل غير صحيح في TypeScript
\n\nقد لا يفهم TypeScript فورًا ما تفعله مع map غير المتزامن. خاصة إذا كنت تستخدم Promise.all دون تحديد النوع بشكل صريح.
المشكلة
\n// ❌ TypeScript يشتكي أو يستنتج نوعًا خاطئًا\nconst ids = [1, 2, 3];\nconst results = await Promise.all(\n ids.map(async (id) => {\n const data = await fetchData(id);\n return data.name; // قد يعتقد TypeScript أن هذا هو Promise<string>\n })\n);\n// results له نوع unknown أو Promise<string>[]\n\nالحل: التعليق الصريح على النوع
\n// ✅ نحدد نوع النتيجة بشكل صريح