TypeScriptで非同期処理をマスター!Promiseとasync/awaitの型安全な使い方
生徒
「TypeScriptで、通信が終わるのを待ってから次の処理をする方法を知りたいです。非同期処理って難しそうで不安です。」
先生
「大丈夫ですよ!TypeScriptでは、Promiseやasync/awaitという仕組みを使うことで、時間がかかる処理をスマートに書くことができます。さらに型安全という仕組みを使えば、エラーを未然に防ぐこともできるんですよ。」
生徒
「型安全なユーティリティ関数というのも作れるんですか?」
先生
「はい!便利な道具箱のような関数を作っておくと、開発がとても楽になります。基本から順番に、パソコンに詳しくない方でもわかるように解説していきますね!」
1. 非同期処理とは何かをやさしく解説
プログラミングの世界には、非同期処理という言葉があります。これは、簡単に言うと「待ち時間が発生する作業」のことです。例えば、レストランで料理を注文したときのことを想像してみてください。注文してから料理が届くまでの間、ずっと厨房の前で立って待っているのは効率が悪いですよね?その間に席に座ってスマホを見たり、友達と話したりして、料理ができあがったら受け取るはずです。このように、ある作業が終わるのを待っている間に他の作業を進める仕組みを非同期処理と呼びます。
パソコンの中では、インターネットから大きな画像データをダウンロードしたり、データベースから情報を読み取ったりするときに時間がかかります。もし非同期処理を使わないと、その間パソコンの画面が固まってしまい、操作ができなくなってしまいます。TypeScriptでは、この待ち時間を上手に扱うための特別な書き方が用意されています。
2. Promiseの基本と型定義の役割
非同期処理を扱う上で最も重要なのがPromise(プロミス)です。日本語に訳すと「約束」という意味になります。「今はまだデータがないけれど、将来必ずデータを渡すよ!」という約束の状態を表しています。Promiseには三つの状態があります。一つ目は「まだ結果待ちの状態」、二つ目は「無事に成功した状態」、三つ目は「残念ながら失敗した状態」です。
TypeScriptを使う最大のメリットは、このPromiseに対して「どんな種類のデータが返ってくるか」をあらかじめ決めておけることです。これを型安全と呼びます。例えば、「数字のデータが返ってくるはずなのに、間違えて文字のデータを扱おうとした」ときに、プログラムを実行する前にパソコンがエラーを教えてくれるようになります。これにより、初心者が陥りやすいミスを大幅に減らすことができます。
// 数値を返すと約束する関数の例
const getLuckyNumber = (): Promise<number> => {
return new Promise((resolve) => {
// 1秒待ってからラッキーセブンを返す
setTimeout(() => {
resolve(7);
}, 1000);
});
};
getLuckyNumber().then((num) => {
console.log("あなたの数字は " + num + " です!");
});
あなたの数字は 7 です!
3. asyncとawaitで魔法のように読みやすく書く
Promiseをより簡単に、まるですぐに結果が出るかのように書けるのがasync(エイシンク)とawait(アウェイト)です。関数の前に「async」と書くと、その関数の中で「await」という魔法の言葉が使えるようになります。awaitを使うと、「ここで処理が終わるまでちょっと待ってね」という指示を出すことができます。これにより、複雑だったプログラムの書き順が、上から下へ流れる自然な順番になり、読みやすさが劇的に向上します。
ここで言う関数とは、特定の命令をひとまとめにしたパックのことです。また、引数(ひきすう)とはその関数に渡す情報のことで、返り値(かえりち)とは関数が実行された後に戻ってくる結果のことです。これらの言葉を覚えておくと、プログラミングの理解がぐっと深まります。TypeScriptでは、これらの入り口と出口に型をつけることで、データの迷子を防ぎます。
// あいさつを取得する非同期関数
async function fetchGreeting(name: string): Promise<string> {
// 擬似的な待ち時間を作る
await new Promise(resolve => setTimeout(resolve, 500));
return "こんにちは、" + name + "さん!";
}
async function showMessage() {
console.log("メッセージを取得中...");
const result = await fetchGreeting("たろう");
console.log(result);
}
showMessage();
メッセージを取得中...
こんにちは、たろうさん!
4. 型安全なユーティリティ関数の重要性
ユーティリティ関数とは、いろいろな場所で使い回せる「便利道具」のような関数のことです。例えば、インターネットからデータを取得するとき、毎回同じようなエラー対策を書くのは大変ですよね。そこで、一度だけしっかりとした安全な道具を作っておき、それを使い回すのがプロのやり方です。TypeScriptを使うことで、その道具が「どんな種類の荷物を運ぶためのものか」を厳密に管理できます。
「型安全」という言葉をもう少し詳しく説明しましょう。これは、箱にラベルを貼るようなものです。野菜を入れる箱に間違って魚を入れようとしたとき、TypeScriptが「それは野菜の箱ですよ!」と注意してくれます。非同期処理では、通信の失敗やデータの欠如など、予期せぬトラブルが起こりやすいため、このラベル貼りが非常に重要になってきます。
5. ジェネリクスを使った高度な型の定義方法
より高度なユーティリティ関数を作るためには、ジェネリクスという機能を使います。これは、型を「変数」のように扱える仕組みです。特定の型に固定するのではなく、「使うときに型を決められる」ようにすることで、一つの関数で数字も文字も、複雑なユーザーデータも、何でも安全に扱えるようになります。初心者の方には少し難しく感じるかもしれませんが、「万能な型」を作れる便利な機能だと覚えておいてください。
例えば、API(サーバーからデータを取得する窓口)からの応答をラップする関数を作る場合、どのようなデータ構造が返ってくるかは毎回異なります。ジェネリクスを使えば、その都度適切な型を当てはめることができるため、開発者は安心してコードを書くことができるのです。ここでは、よく使われる「データの取得とエラーハンドリング」をセットにしたユーティリティの例を見てみましょう。
// APIのレスポンスを表現する型
type ApiResponse<T> = {
data: T | null;
error: string | null;
};
// 型安全にデータを取得するユーティリティ関数
async function safeFetch<T>(url: string): Promise<ApiResponse<T>> {
try {
// 本来はここでインターネットからデータを取得しますが、
// 今回はサンプルとして成功したと仮定します
console.log(url + " からデータを取得しています...");
const responseData = { id: 1, name: "サンプル商品" } as T;
return { data: responseData, error: null };
} catch (e) {
return { data: null, error: "取得に失敗しました" };
}
}
async function main() {
type Product = { id: number; name: string };
const result = await safeFetch<Product>("https://example.com/api/item");
if (result.data) {
console.log("商品名: " + result.data.name);
}
}
main();
https://example.com/api/item からデータを取得しています...
商品名: サンプル商品
6. タイムアウト処理を追加してさらに安全にする
非同期処理では、いつまで経っても返事が来ないという困った事態も起こり得ます。これを「フリーズ」や「無限待ち」と呼びます。これを防ぐために、一定時間が経過したら強制的に終了させるタイムアウトという機能をユーティリティ関数に追加してみましょう。これもTypeScriptの型定義をしっかり行うことで、時間の管理とデータの取得を両立させることができます。
「タイムアウト」は、日常生活でもよくある「制限時間」のことです。5秒経っても返事がなかったら諦めて次の行動に移る、という処理をプログラミングで作ることで、アプリ全体の動作が軽快になります。こうした細かい配慮が、使いやすいシステムを作るための秘訣です。以下の例では、指定した時間内に処理が終わらない場合にエラーを出す仕組みを紹介します。
// 指定した時間(ミリ秒)だけ待つ関数
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// タイムアウト付きの非同期処理ユーティリティ
async function withTimeout<T>(task: Promise<T>, timeoutMs: number): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("時間切れです!")), timeoutMs);
});
// 実際の処理とタイムアウトのどちらか早い方を採用する
return Promise.race([task, timeout]);
}
async function runTask() {
try {
const slowTask = delay(3000).then(() => "完了!");
const result = await withTimeout(slowTask, 2000);
console.log(result);
} catch (err) {
if (err instanceof Error) {
console.log("エラー発生: " + err.message);
}
}
}
runTask();
エラー発生: 時間切れです!
7. エラーを恐れない例外処理の書き方
プログラミングにおいて、エラーは決して悪いことではありません。大切なのは、エラーが起きたときに「どうやって親切に教えるか」です。これを例外処理と呼びます。TypeScriptではtry...catchというブロックを使って、エラーをキャッチして安全に処理を続けることができます。非同期処理のユーティリティ関数の中にこの仕組みを組み込んでおくことで、どこでエラーが起きてもアプリが壊れない頑丈な仕組みを構築できます。
たとえば、サーバーの電源が切れていた、パスワードが間違っていたなど、エラーの理由は様々です。これらを適切に分類して、ユーザーに分かりやすいメッセージを表示することが求められます。型定義を使ってエラーの種類を分類しておけば、どのタイプのエラーに対しても、TypeScriptが最適な解決策を提案してくれるようになります。これが、型安全な開発の醍醐味です。
8. 実践的なユーティリティ関数の活用シーン
これまで学んできたことを組み合わせると、実際の現場で即戦力となるユーティリティ関数が出来上がります。例えば、スマートフォンのアプリでニュースを表示するとき、まずローディング画面を表示し、裏でデータを取得し、成功したらリストを表示し、失敗したら「再試行」ボタンを出すといった一連の流れがあります。これらすべてを型安全な関数で管理することで、バグの少ない高品質なアプリが作れるようになります。
パソコンを初めて触る方にとっては、最初は記号や英単語が多くて戸惑うかもしれません。しかし、一つ一つのパーツには必ず意味があり、それは私たちの日常生活の論理と同じです。TypeScriptの非同期処理は、最初は難しく感じますが、慣れてくるとこれほど心強い味方はありません。便利なユーティリティ関数を自分で作り、それを組み合わせて大きなプログラムを組み立てていく楽しさを、ぜひ味わってみてください。