【ITニュース解説】React + TypeScript: async functions
2025年09月16日に「Dev.to」が公開したITニュース「React + TypeScript: async functions」について初心者にもわかりやすく解説しています。
ITニュース概要
ReactとTypeScriptのasync関数は、サーバー通信など時間のかかる処理で非同期実行を行う。戻り値は常にPromise<T>型となる。awaitは処理を一時停止しコードを読みやすくするが、イベントループはブロックせず、他の処理は継続する。
ITニュース解説
システムエンジニアを目指す上で、現代のWebアプリケーション開発、特にReactとTypeScriptを使う際には、非同期処理の理解が不可欠となる。その中心にあるのがasync関数である。このasync関数がどのように機能し、どのような戻り値を持つのか、そしてawaitキーワードとどのように連携するのかを具体的に理解することは、堅牢で効率的なアプリケーションを構築するために非常に重要だ。
非同期処理が必要となる典型的なシナリオは、ユーザーがログインしているかどうかをサーバーに問い合わせる場合である。もし、この処理が瞬時に完了し、結果が常にすぐに手に入るのであれば、以下のような同期的な関数で十分に対応できる。
1function isUserLoggedIn(): boolean { 2 return true; // 例として常にtrueを返す 3}
しかし、現実のアプリケーションでは、ログイン状態の確認はサーバーへのネットワークリクエストを伴うことがほとんどだ。ネットワーク通信には時間がかかり、その間アプリケーションの他の処理を停止させるわけにはいかない。ここで非同期処理の出番となる。asyncキーワードを使って関数を非同期化すると、その関数は以下のようになる。
1async function isUserLoggedIn(): Promise<boolean> { 2 const response = await fetch("/api/check-login"); 3 4 if (!response.ok) { 5 return false; 6 } 7 8 const data = await response.json(); 9 return data.loggedIn; // サーバーからのログイン状態を返す 10}
この非同期版の関数を見ると、戻り値の型がbooleanではなくPromise<boolean>になっていることに気づくだろう。これがasync関数における最も重要なポイントの一つである。asyncキーワードが付与された関数は、たとえ関数内でreturn trueやreturn falseのように直接的な値を返しているように見えても、実際にはその値をPromiseで包んで返す。具体的には、return true;と記述した場合、内部的にはPromise.resolve(true);が実行されているのと同じ意味になる。したがって、async関数の戻り値の型は常にPromise<T>(Tは実際に返される値の型)として宣言しなければならない。もしこれを誤ってasync function isUserLoggedIn(): boolean { ... }のようにbooleanと宣言してしまうと、Type 'boolean' is not assignable to type 'Promise<boolean>'.というコンパイルエラーが発生し、TypeScriptが型の一貫性を保つ手助けをしてくれる。
非同期関数がPromiseを返すので、その結果を利用するには、awaitキーワードを使うか、.then()メソッドを使うかのどちらかの方法を用いる。
awaitを使う場合、const loggedIn = await isUserLoggedIn();のように記述する。このawaitは、isUserLoggedIn()関数が返すPromiseが解決される(つまり、非同期処理が完了し、結果が得られる)まで、現在の関数内の実行を一時停止させる。awaitの後に、Promiseが解決した結果であるtrueかfalseがloggedIn変数に代入される。
もう一つの方法は、.then()メソッドを使うことである。isUserLoggedIn().then(status => console.log(status));のように記述する。これは、isUserLoggedIn()が返すPromiseが解決された時に、引数に指定したコールバック関数が実行され、その結果がstatusとして渡される。どちらの方法も非同期処理の結果を扱うためのものだが、awaitを使った方が同期的なコードのように読みやすく、エラーハンドリングも容易になるため、現代のJavaScript/TypeScriptではawaitが一般的に推奨される。
ここでよくある誤解として、「awaitを使うと処理が同期的に戻ってしまうのではないか」という疑問がある。答えは「いいえ」である。awaitは確かに現在の関数内の処理を一時停止させるが、プログラム全体の実行、特にJavaScriptのイベントループをブロックするわけではない。イベントループとは、JavaScriptが非同期処理を含めたタスクをどのように実行していくかを管理する仕組みである。awaitがPromiseの解決を待っている間も、イベントループは他のタスク(例えば、ユーザーインターフェースの更新や他の非同期処理の開始)を実行し続けることができる。
具体的な例で考えてみよう。
1async function main() { 2 console.log("Before"); 3 await isUserLoggedIn(); // ここで現在の関数は一時停止 4 console.log("After"); 5} 6 7main(); 8console.log("Outside");
このコードを実行すると、出力は通常以下の順序になる。
Before
Outside
After
この出力順序は、isUserLoggedIn()がawaitによって待機している間も、console.log("Outside");が実行されていることを示している。これは、awaitが局所的に、つまりmain関数の中だけを一時停止させ、イベントループ全体をブロックしないことの明確な証拠である。つまり、async関数はあくまで非同期処理であり、awaitはその非同期処理の結果をより同期的なコードのように扱えるようにするための構文糖衣(シンタックスシュガー)に過ぎない。
asyncキーワードは、一般的な関数だけでなく、アロー関数(無名関数を簡潔に記述する方法)ともシームレスに連携する。これは現代のTypeScriptやReactプロジェクトで非常に一般的である。例えば、ログイン状態チェック関数をアロー関数で記述すると以下のようになる。
1const isUserLoggedIn = async (): Promise<boolean> => { 2 const response = await fetch("/api/check-login"); 3 const data = await response.json(); 4 return data.loggedIn; 5};
Reactのイベントハンドラでも、このasyncアロー関数は頻繁に利用される。フォームの送信処理などで非同期的なAPI呼び出しを行う場合、以下のように記述できる。
1const handleSubmit = async (event: React.FormEvent) => { 2 event.preventDefault(); // デフォルトのフォーム送信を防ぐ 3 4 const success = await isUserLoggedIn(); 5 console.log("Login status:", success); 6};
さらに、配列のメソッド、特にmapのような関数とasyncアロー関数を組み合わせることで、複数の非同期処理を効率的に扱うことができる。例えば、複数のURLからデータを並行して取得したい場合、Promise.allと組み合わせるのが一般的だ。
1const urls = ["/a", "/b", "/c"]; 2 3const fetchAll = async () => { 4 const results = await Promise.all( 5 urls.map(async url => { // map内でasyncアロー関数を使用 6 const res = await fetch(url); 7 return res.json(); 8 }) 9 ); 10 11 console.log(results); // 全てのURLからの結果が配列として得られる 12};
Promise.allは、引数に渡された全てのPromiseが解決されるまで待機し、その結果を配列として返す。これにより、複数の非同期処理を並行して実行し、その全てが完了するのを効率的に待つことができる。
まとめると、async関数は常にPromise<T>を返し、直接的な値Tではない。関数内で値を返すと、それは自動的にPromise.resolve(value)としてPromiseにラップされる。awaitキーワードは、そのawaitが書かれた関数内の実行を一時停止させるが、JavaScriptのイベントループ全体をブロックするわけではないため、他の非同期処理やタスクは引き続き実行され続ける。asyncアロー関数は、コールバック関数やイベントハンドラ、そして配列の操作など、様々な場面で活用される。APIとの通信やデータベースへのアクセス、あるいは時間のかかるファイル操作など、将来的に結果が得られる可能性のあるあらゆる非同期処理に対しては、async関数を使ってそのシグネチャ(関数の定義)を適切に設計することが、コードの可読性とメンテナンス性を高め、将来的な拡張性も確保するために不可欠である。