Funkcja async w TypeScript zawsze zwraca obietnicę: jeśli jej ciało zwraca string, jej typ to Promise<string>. Wewnątrz niej await rozpakowuje Promise<T> do T.
Zachowanie w czasie działania (co wstrzymuje await, pętla zdarzeń) należy do JavaScriptu i opisuje je strona async/await w JavaScript. Rola TypeScriptu to typy na wejściu i wyjściu.
Typy zwracane funkcji async
Zadeklarowany typ zwracany funkcji async musi być Promise<...>, choć ciało zwraca zwykłą wartość. Jeśli go pominiesz, TypeScript go wywnioskuje.
Zapis async function count(): number to błąd TS1064 (The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?). Typ pomocniczy Awaited<T> daje typ po rozpakowaniu: Awaited<ReturnType<typeof count>> to number.
Zapomniane await
Brakujące await zostawia cię z Promise<T> zamiast T. TypeScript wyłapuje większość takich przypadków, bo typy przestają pasować:
Błąd z if (ok) jest prawdziwy: obiekt obietnicy zawsze jest prawdziwy logicznie, więc dostęp został przyznany. TypeScript zgłasza to jako TS2801. Przypisanie obietnicy do zmiennej boolean albo odczyt właściwości, której obietnica nie ma, też by się nie skompilowały. Wywołanie, którego wynik ignorujesz (save(user);), nie zostaje wyłapane; zajmuje się tym reguła no-floating-promises z typescript-eslint.
Obsługa błędów przez try/catch
Odrzucona obietnica sprawia, że await rzuca wyjątek, więc zwykłe try/catch działa. Przy strict zmienna w catch ma typ unknown i trzeba ją zawęzić, zanim odczytasz .message:
throw wewnątrz funkcji async odrzuca jej obietnicę zamiast rzucać wyjątek w miejscu wywołania. Więcej wzorców, w tym własne klasy błędów i zwracanie wyników zamiast rzucania wyjątków, znajdziesz na stronie o obsłudze błędów.
await na najwyższym poziomie
await poza jakąkolwiek funkcją działa tylko w module ES. Plik kompilowany jako CommonJS (tak jest w tych przykładach i w projektach Node bez "type": "module") go odrzuca:
index.ts(2,14): error TS1309: The current file is a CommonJS module and cannot use 'await' at the top level.
Opakuj kod w asynchroniczną funkcję main i wywołaj ją, tak jak robi każdy przykład na tej stronie. W projekcie z modułami ES ("type": "module" w package.json z module ustawionym na node16 albo nodenext, albo module: "esnext" dla bundlera) await na najwyższym poziomie jest dozwolone.
Po kolei a równolegle
Każde await czeka na swoją obietnicę, zanim zacznie się następna linia. Dla niezależnych wywołań najpierw uruchom je wszystkie, a potem poczekaj na nie razem przez Promise.all:
W części sekwencyjnej b nie może skończyć się przed a, bo jeszcze się nie zaczęło. W części równoległej d kończy się pierwsze, a łączny czas to mniej więcej czas najdłuższego wywołania, a nie suma. Promise.all i tak zwraca wyniki w kolejności wejścia, otypowane jako krotka.
Pułapka z forEach
forEach ignoruje obietnicę zwracaną przez asynchroniczną funkcję zwrotną, więc nic nie czeka na zakończenie pracy:
for...of z await przetwarza elementy pojedynczo; Promise.all z map uruchamia je równolegle i czeka na wszystkie. forEach nie robi żadnej z tych rzeczy, a TypeScript nie ostrzega, bo funkcja zwrotna otypowana jako zwracająca void akceptuje funkcję, która zwraca obietnicę.
Asynchroniczna iteracja z for await
for await...of przechodzi po asynchronicznym obiekcie iterowalnym, na przykład po asynchronicznym generatorze, czekając na każdą wartość:
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();
Typ elementu pochodzi z adnotacji AsyncGenerator<T> generatora albo z wnioskowania, gdy ją pominiesz.
Najczęściej zadawane pytania
Jaki typ zwraca funkcja async w TypeScript?
Zawsze obietnicę. Funkcja async, która zwraca number, ma typ zwracany Promise<number>, a taka, która nic nie zwraca, ma Promise<void>. Zapis async function f(): number to błąd TS1064, który podpowiada Promise<number>.
Jak użyć await na najwyższym poziomie w TypeScript?
await na najwyższym poziomie działa tylko w module ES, gdy module ma wartość es2022, esnext, system, preserve albo node16/node18/node20/nodenext w pliku, który Node traktuje jako ESM, a target to co najmniej es2017. W pliku CommonJS to błąd TS1309. Przenośne rozwiązanie to asynchroniczna funkcja main: async function main() { ... } main();.
Jak obsługiwać błędy z async/await w TypeScript?
Opakuj await w try/catch. Przy strict przechwycona wartość ma typ unknown, więc najpierw ją zawęź: if (e instanceof Error) console.log(e.message). Odrzucenie, na które nikt nie czeka i którego nikt nie łapie, staje się nieobsłużonym odrzuceniem.
Jak uruchomić wywołania async równolegle w TypeScript?
Najpierw uruchom wszystkie obietnice, a potem poczekaj na nie razem: const [a, b] = await Promise.all([loadA(), loadB()]). Zapis await loadA(); await loadB(); wykonuje je jedno po drugim. Promise.all zachowuje typ każdego wyniku w otrzymanej krotce.
Dlaczego async nie działa wewnątrz forEach?
forEach wywołuje funkcję zwrotną i ignoruje to, co ona zwraca, więc nikt nie czeka na obietnice z asynchronicznej funkcji zwrotnej: pętla kończy się od razu, a kod po niej wykonuje się, zanim praca się skończy. Do pracy po kolei użyj for...of z await, a do pracy równoległej await Promise.all(items.map(async (x) => ...)).