Menu

Async Await ב-TypeScript: טיפוסים, שגיאות וקריאות במקביל

איך async ו-await מקבלים טיפוסים ב-TypeScript: פונקציית async מחזירה Promise<T>, await פורס אותו, שגיאות נתפסות עם try/catch, await ברמה העליונה דורש ES module, וההבדל בין המתנה אחד אחרי השני, המתנה במקביל והמלכודת של forEach.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

פונקציית async ב-TypeScript תמיד מחזירה promise: אם הגוף שלה מחזיר string, הטיפוס שלה הוא Promise<string>. בתוכה, await פורס Promise<T> ל-T.

ההתנהגות בזמן ריצה (מה await משהה, ה-event loop) שייכת ל-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);) לא נתפסת; הכלל no-floating-promises של typescript-eslint מכסה את זה.

טיפול בשגיאות עם try/catch

promise שנדחה גורם ל-await לזרוק, ולכן try/catch רגיל עובד. תחת strict, המשתנה ב-catch הוא unknown, ומצמצמים אותו לפני שקוראים את .message:

throw בתוך פונקציית async דוחה את ה-promise שלה במקום לזרוק במקום הקריאה. דפוסים נוספים, כולל מחלקות שגיאה מותאמות והחזרת תוצאות במקום זריקה, נמצאים בעמוד טיפול בשגיאות.

await ברמה העליונה

await מחוץ לכל פונקציה עובד רק ב-ES module. קובץ שמקומפל כ-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 module ("type": "module" ב-package.json עם module שמוגדר ל-node16 או nodenext, או module: "esnext" ל-bundler), await ברמה העליונה מותר.

סדרתי מול מקביל

כל await ממתין ל-promise שלו לפני שהשורה הבאה מתחילה. לקריאות בלתי תלויות, התחילו את כולן קודם והמתינו להן יחד עם Promise.all:

בחצי הסדרתי, b לא יכול להסתיים לפני a כי הוא עוד לא התחיל. בחצי המקבילי, d מסתיים ראשון, והזמן הכולל הוא בערך הקריאה הארוכה ביותר ולא הסכום. Promise.all עדיין מחזיר את התוצאות בסדר הקלט, עם טיפוס של tuple.

המלכודת של forEach

forEach מתעלמת מה-promise ש-callback אסינכרוני מחזיר, ולכן אף אחד לא ממתין לעבודה:

for...of עם await מעבד פריטים אחד אחרי השני; Promise.all עם map מריץ אותם במקביל וממתין לכולם. forEach לא עושה אף אחד מהשניים, ו-TypeScript לא מזהירה, כי callback שהטיפוס שלו מחזיר void מקבל גם callback שמחזיר promise.

איטרציה אסינכרונית עם for await

for await...of עוברת בלולאה על async iterable, כמו async generator, וממתינה לכל ערך:

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> של ה-generator, או מהסקה כשמשמיטים אותה.

שאלות נפוצות

מה טיפוס ההחזרה של פונקציית async ב-TypeScript?

תמיד promise. פונקציית async שמחזירה number מקבלת את טיפוס ההחזרה Promise<number>, ופונקציה שלא מחזירה כלום מקבלת Promise<void>. כתיבה של async function f(): number היא השגיאה TS1064, שמציעה Promise<number>.

איך משתמשים ב-await ברמה העליונה ב-TypeScript?

await ברמה העליונה עובד רק ב-ES module, כש-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). דחייה (rejection) שאף אחד לא ממתין לה ולא תופס אותה הופכת ל-unhandled rejection.

איך מריצים קריאות async במקביל ב-TypeScript?

התחילו קודם את כל ה-promises, ואז המתינו להם יחד: const [a, b] = await Promise.all([loadA(), loadB()]). כתיבה של await loadA(); await loadB(); מריצה אותן אחת אחרי השנייה. Promise.all שומר את הטיפוס של כל תוצאה ב-tuple שמתקבל.

למה async לא עובד בתוך forEach?

forEach קוראת ל-callback ומתעלמת ממה שהוא מחזיר, ולכן אף אחד לא ממתין ל-promises של callback אסינכרוני: הלולאה מסתיימת מיד, והקוד שאחריה רץ לפני שהעבודה הסתיימה. השתמשו ב-for...of עם await לעבודה אחד אחרי השני, או ב-await Promise.all(items.map(async (x) => ...)) לעבודה במקביל.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל