TypeScriptでasync/awaitを関数型プログラミングで書く!非同期処理の初心者ガイド
生徒
「TypeScriptで非同期処理をスマートに書きたいのですが、async/awaitと関数型プログラミングを組み合わせるにはどうすればいいですか?」
先生
「非同期処理を関数型らしく書くには、データの流れを意識することが大切です。Promiseをうまく繋いでいく方法を学びましょう。」
生徒
「関数型プログラミングって難しそうですが、初心者でも使いこなせますか?」
先生
「大丈夫ですよ。まずはasyncやawaitの基本から、スッキリ書くためのテクニックまで順番に解説していきますね!」
1. 非同期処理とPromiseの基本を知ろう
プログラミングの世界には、処理が終わるのを待たずに次の命令に進む「非同期処理」という仕組みがあります。例えば、インターネットから大きな画像データをダウンロードする場合、ダウンロードが終わるまで画面が固まってしまったら困りますよね。そうした待ち時間が発生する処理を、裏側で進めておき、終わったら結果を受け取る仕組みが非同期処理です。
TypeScriptやJavaScriptでは、この予約票のような役割を果たすのがPromise(プロミス)というオブジェクトです。Promiseは「将来、値が返ってくることを約束するもの」という意味です。成功したときは解決(resolve)、失敗したときは拒否(reject)という状態になります。パソコンを触ったことがない方でも、レストランで注文をしたあとに渡される「呼び出しベル」をイメージすると分かりやすいでしょう。料理ができるまで自由に過ごせて、準備ができたらベルが鳴って知らせてくれます。この仕組みをコードで表現するのが第一歩です。
2. asyncとawaitの役割を理解する
Promiseをより人間にとって読みやすく、直感的に書けるようにしたのがasync(エイシンク)とawait(アウェイト)です。関数にasyncをつけると、その関数は自動的にPromiseを返すようになります。そして、その関数の中でawaitを使うと、非同期処理が終わるまでその場で行を止めて待機してくれます。
これを使うことで、複雑な「予約票」のやり取りを、まるで上から下へ順番に実行される普通のプログラムのように書くことができます。awaitを使わないと、処理が終わる前に次の命令が動いてしまい、まだ空っぽのデータを使おうとしてエラーになってしまいます。初心者のうちは、時間のかかる処理(通信やファイルの読み込みなど)にはawaitをつける、というルールを覚えるのが上達の近道です。
async function fetchUser() {
// 擬似的な通信待ち(2秒待つ)
const response = await new Promise((resolve) => {
setTimeout(() => resolve("田中太郎"), 2000);
});
console.log(response);
}
fetchUser();
田中太郎
3. 関数型プログラミングの考え方を取り入れる
関数型プログラミングとは、一言で言うと「データを加工する専門の道具(関数)を組み合わせて、大きな処理を作る」手法のことです。重要なルールは、元のデータを勝手に書き換えないこと(不変性)と、同じ入力を与えれば必ず同じ結果が返ってくること(純粋性)です。これにより、プログラムの動きが予測しやすくなり、バグが減ります。
非同期処理において関数型の考え方を取り入れるとは、awaitで取得したデータを加工して、次の処理へ流していくパイプラインを作るようなイメージです。if文を並べて処理を分岐させるのではなく、データの変換を関数で表現することで、コードが美しく、再利用しやすくなります。TypeScriptの強力な型チェック機能を使うことで、どの関数にどんなデータが流れているかを常に把握できるのが大きなメリットです。
4. 非同期処理を配列操作のように扱うテクニック
関数型プログラミングでは、配列のデータを変換するmapなどのメソッドを多用します。非同期処理でも同様に、複数の処理をまとめて行いたい場合があります。例えば、複数のユーザー情報を同時に取得して、それらを一斉に加工する場合です。ここで便利なのがPromise.allです。これは複数のPromiseを一つにまとめ、すべてが完了するのを待つ機能です。
これを関数型のスタイルで書くと、データのリストに対して「非同期で取得する関数」を適用し、その結果を一気に処理する形になります。バラバラにawaitを繰り返すよりも効率が良く、コードの見通しも良くなります。配列の各要素に対して非同期な処理を適用し、その結果を新しい配列として受け取る一連の流れは、現代のTypeScript開発において非常によく使われるテクニックです。
const userIds = [1, 2, 3];
// IDからユーザー名を取得する関数
const getName = async (id: number) => {
return `ユーザー${id}`;
};
// 関数型のアプローチ:mapでPromiseの配列を作り、allで待機する
const fetchAllNames = async () => {
const promises = userIds.map(id => getName(id));
const names = await Promise.all(promises);
console.log(names);
};
fetchAllNames();
["ユーザー1", "ユーザー2", "ユーザー3"]
5. パイプラインでデータを繋ぐ方法
関数型プログラミングの真骨頂は、複数の小さな関数を繋いで、一つの大きな目的を達成することです。これを「合成」と呼びます。非同期処理においても、Aを取得して、その結果を使ってBを取得し、最後にCとして出力する、という流れをスムーズに記述したいものです。
async/awaitを使いつつ、このパイプラインを表現するには、各ステップを独立した関数として定義します。それぞれの関数は一つのことだけを行い、awaitを使って同期的に見えるように繋ぎます。これにより、どこでエラーが起きても原因が特定しやすくなり、後から処理を一部だけ入れ替えることも簡単になります。プログラミング未経験の方でも、料理の工程(野菜を切る、炒める、盛り付ける)を一つずつ分担して進める様子を想像すると理解しやすいはずです。
// 1. データを取得する
const fetchData = async () => ({ value: 100 });
// 2. データを加工する関数
const doubleValue = (data: { value: number }) => data.value * 2;
// 3. 結果を表示する関数
const display = (num: number) => console.log(`結果は${num}です`);
// メインの処理(パイプライン)
const runProcess = async () => {
const data = await fetchData();
const result = doubleValue(data);
display(result);
};
runProcess();
結果は200です
6. 非同期エラーハンドリングも関数型でスッキリ
非同期処理には失敗がつきものです。ネットが繋がらなかったり、データが見つからなかったりします。通常はtry-catchという構文を使ってエラーを捕まえますが、関数型の世界ではエラーも「戻り値」の一つとして扱うテクニックがあります。
例えば、成功したときはデータを返し、失敗したときはエラーオブジェクトを返すような設計にします。これにより、プログラムが途中で突然止まってしまうことを防ぎ、安全に処理を継続できます。TypeScriptでは「この関数は成功か失敗のどちらかを返す」という型を定義できるため、型に守られながら安全にエラー対応を行うことができます。初心者が一番苦労する「原因不明の停止」を回避するために、非常に有効な手法です。
7. 実践的な非同期データの変換処理
最後に、より実用的な例を見てみましょう。サーバーから受け取った生データを、画面に表示しやすい形に変換する処理です。ここでは、非同期で取得したリストに対して、特定の条件でフィルタリングを行い、必要な形に変換して返します。これら全ての工程をasync/awaitと関数型のメソッドで組み合わせることで、宣言的(何をしたいかが明確)なコードになります。
「どうやって処理するか」という手順を細かく書くのではなく、「どんな結果が欲しいか」を記述するのがモダンなTypeScriptの書き方です。一見難しく見えるかもしれませんが、一つ一つの関数は非常にシンプルなので、読み解く力さえ身につければ、誰でも高品質なプログラムを書くことができます。まずは短いコードから真似をして、データの流れを追いかける練習をしてみましょう。
interface Item {
id: number;
price: number;
}
const getItems = async (): Promise<Item[]> => {
return [{ id: 1, price: 1000 }, { id: 2, price: 2000 }, { id: 3, price: 3000 }];
};
const processItems = async () => {
const items = await getItems();
// 関数型のスタイルでデータを加工
const expensiveItems = items
.filter(item => item.price >= 2000) // 2000円以上のものだけ選ぶ
.map(item => `商品${item.id}: ${item.price}円`); // 文字列に変換
expensiveItems.forEach(msg => console.log(msg));
};
processItems();
商品2: 2000円
商品3: 3000円