Menu
flag Ar iconالعربيةdown icon

Async Await في TypeScript: الأنواع والأخطاء والاستدعاء المتوازي

كيف تُحدد أنواع async وawait في TypeScript: الدالة async تعيد Promise<T>، وawait تفكّها، والأخطاء تُلتقط بـ try/catch، وawait على المستوى الأعلى تحتاج إلى وحدة ES، والفرق بين الانتظار واحدًا تلو الآخر والانتظار المتوازي، وفخ forEach.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

الدالة async في TypeScript تعيد promise دائمًا: إذا أعاد جسمها string فنوعها Promise<string>. وداخلها تفكّ await الـ Promise<T> إلى T.

سلوك وقت التشغيل (ما الذي توقفه await، وحلقة الأحداث) هو JavaScript، ومشروح في async/await في JavaScript. دور TypeScript هو الأنواع الداخلة والخارجة.

أنواع الإعادة للدوال Async

يجب أن يكون نوع الإعادة المصرّح به لدالة async هو Promise<...>، مع أن الجسم يعيد القيمة العادية. يستنتجه TypeScript إذا لم تكتبه.

كتابة async function count(): number خطأ TS1064 (The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?). ويعطي النوع المساعد Awaited<T> النوع بعد فكّه: Awaited<ReturnType<typeof count>> هو number.

نسيان await

غياب await يتركك مع Promise<T> بدلًا من T. يكتشف TypeScript معظم هذه الحالات لأن الأنواع لم تعد متوافقة:

خطأ if (ok) حقيقي: كائن الـ promise قيمته truthy دائمًا، لذلك منح الوصول. يبلّغ TypeScript عنه كـ TS2801. وإسناد الـ promise إلى متغير boolean، أو قراءة خاصية ليست في الـ promise، سيفشلان في الترجمة أيضًا. أما الاستدعاء الذي تتجاهل نتيجته (save(user);) فلا يُكتشف؛ وتغطيه قاعدة typescript-eslint المسماة no-floating-promises.

معالجة الأخطاء بـ try/catch

الـ promise المرفوض يجعل await ترمي خطأ، لذلك يعمل try/catch العادي. مع strict يكون المتغير في catch من النوع unknown، فتضيّقه قبل قراءة .message:

throw داخل دالة async ترفض الـ promise الخاص بها بدلًا من رمي خطأ في موضع الاستدعاء. أنماط أخرى، منها أصناف الأخطاء المخصصة وإعادة النتائج بدلًا من الرمي، موجودة في صفحة معالجة الأخطاء.

await على المستوى الأعلى

لا تعمل await خارج أي دالة إلا في وحدة ES. الملف المُترجم كـ CommonJS (وهي حالة هذه الأمثلة، وحالة مشاريع Node التي ليس فيها "type": "module") يرفضها:

index.ts(2,14): error TS1309: The current file is a CommonJS module and cannot use 'await' at the top level.

ضع الكود داخل دالة main غير متزامنة واستدعها، كما يفعل كل مثال في هذه الصفحة. في مشروع يعمل بوحدات ES ("type": "module" في package.json مع ضبط module على node16 أو nodenext، أو module: "esnext" مع أداة تجميع) تكون await على المستوى الأعلى مسموحة.

التتابع والتوازي

كل await تنتظر الـ promise الخاص بها قبل أن يبدأ السطر التالي. للاستدعاءات المستقلة، ابدأها كلها أولًا وانتظرها معًا بـ Promise.all:

في النصف المتتابع لا يمكن أن ينتهي b قبل a لأنه لم يبدأ بعد. وفي النصف المتوازي ينتهي d أولًا، ويكون الوقت الكلي قرابة مدة أطول استدعاء بدلًا من مجموع المدد. ومع ذلك يعيد Promise.all النتائج بترتيب المُدخلات، بنوع tuple.

فخ forEach

تتجاهل forEach الـ promise الذي تعيده دالة async، فلا شيء ينتظر العمل:

تعالج for...of مع await العناصر واحدًا تلو الآخر؛ وPromise.all مع map تشغّلها بالتوازي وتنتظرها كلها. أما forEach فلا تفعل هذا ولا ذاك، ولا يحذّر TypeScript، لأن دالة نوعها يعيد void تقبل دالة تعيد promise.

التكرار غير المتزامن بـ for await

تكرر for await...of على كائن قابل للتكرار غير متزامن، مثل مولّد غير متزامن، وتنتظر كل قيمة:

async function* pages(total: number): AsyncGenerator<string[]> {
    for (let page = 1; page <= total; page++) {
        await new Promise((r) => setTimeout(r, 10));
        yield [`item ${page}a`, `item ${page}b`];
    }
}

async function main() {
    for await (const batch of pages(3)) {
        console.log(batch.join(", ")); // batch: string[]
    }
}
main();

نوع العنصر يأتي من تحديد AsyncGenerator<T> للمولّد، أو من الاستنتاج إذا لم تكتبه.

الأسئلة الشائعة

ما نوع الإعادة للدالة async في TypeScript؟

دائمًا promise. الدالة async التي تعيد number نوع إعادتها Promise<number>، والتي لا تعيد شيئًا نوعها Promise<void>. كتابة async function f(): number خطأ TS1064، ويقترح المترجم Promise<number>.

كيف أستخدم await على المستوى الأعلى في TypeScript؟

لا تعمل await على المستوى الأعلى إلا في وحدة ES، مع ضبط module على es2022 أو esnext أو system أو preserve، أو node16/node18/node20/nodenext في ملف تعامله Node كـ ESM، ومع target بقيمة es2017 أو أعلى. في ملف CommonJS تكون خطأ TS1309. الحل الذي يعمل في كل مكان هو دالة main غير متزامنة: async function main() { ... } main();.

كيف أعالج الأخطاء مع async/await في TypeScript؟

ضع await داخل try/catch. مع strict يكون نوع القيمة الملتقطة unknown، لذلك ضيّقها أولًا: if (e instanceof Error) console.log(e.message). والرفض الذي لا يُنتظر ولا يُلتقط أبدًا يصبح رفضًا غير معالج.

كيف أشغّل استدعاءات async بالتوازي في TypeScript؟

ابدأ كل الـ promises أولًا، ثم انتظرها معًا: const [a, b] = await Promise.all([loadA(), loadB()]). كتابة await loadA(); await loadB(); تشغّلها واحدًا تلو الآخر. ويحتفظ Promise.all بنوع كل نتيجة في الـ tuple الناتج.

لماذا لا تعمل async داخل forEach؟

تستدعي forEach الدالة وتتجاهل ما تعيده، لذلك لا تُنتظر الـ promises القادمة من دالة async أبدًا: تنتهي الحلقة فورًا ويعمل الكود الذي بعدها قبل انتهاء العمل. استخدم for...of مع await للعمل واحدًا تلو الآخر، أو await Promise.all(items.map(async (x) => ...)) للعمل المتوازي.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن