TypeScriptで非同期関数の戻り値に型をつける方法を徹底解説!Promiseとasync/awaitの基本
生徒
「TypeScriptで非同期処理の結果に型をつけたいのですが、どうすればいいですか?」
先生
「非同期処理、つまり時間がかかる処理の結果を扱うときは、Promiseという型を使います。asyncとawaitを使うと、より読みやすく書けますよ。」
生徒
「Promiseって少し難しそうですね。具体的な書き方を教えてもらえますか?」
先生
「大丈夫ですよ。まずは基本の型定義から、一つずつ丁寧に見ていきましょう!」
1. 非同期処理とPromiseの基本を知ろう
プログラミングの世界には、処理が終わるまで待たなければならない作業があります。例えば、インターネットからデータをダウンロードしたり、大きなファイルを読み込んだりする場合です。これを非同期処理と呼びます。TypeScriptでこの非同期処理の結果を表現するために使われるのがPromise(プロミス)です。
Promiseとは、文字通り「約束」という意味です。「今はまだデータがないけれど、将来的にデータが準備できたら渡すよ」という状態を指します。TypeScriptでは、この約束されたデータが最終的にどのような形(型)になるのかを事前に決めておくことができます。これにより、後からデータが届いたときに、型が合わなくてプログラムが動かなくなるというミスを防ぐことができます。パソコンをあまり触ったことがない方でも、予約注文をした商品が後で届くようなイメージを持つと理解しやすいでしょう。
2. 非同期関数の戻り値に型をつける書き方
TypeScriptで非同期関数の戻り値に型を指定する場合、関数名の後ろにPromise<型名>という形式で記述します。これが基本的なルールです。例えば、最終的に数字を返す関数であれば、戻り値の型はPromise<number>となります。文字列であればPromise<string>です。
もし戻り値の型を指定しないと、TypeScriptは自動的に型を推論しようとしますが、複雑なプログラムになるとエラーの原因を見つけるのが難しくなります。そのため、戻り値を明確に書くことは、安全なプログラムを作るための第一歩となります。ジェネリクスと呼ばれる< >記号の中に、将来手に入るデータの型を入れるのがポイントです。それでは、具体的なプログラムを見てみましょう。
// 数値を返す非同期関数の例
async function getRandomNumber(): Promise<number> {
return 100;
}
// 実行して結果を受け取る
getRandomNumber().then((value) => {
console.log(value);
});
実行結果は以下のようになります。
100
3. asyncとawaitの仕組みと役割
非同期処理をより直感的に書くために登場したのが、async(エイシンク)とawait(アウェイト)です。これらはセットで使用されます。関数の前にasyncをつけることで、その関数は自動的にPromiseを返すようになります。そして、その関数の中で時間の掛かる処理を待つときにawaitを使います。
awaitを使うと、非同期処理が終わるまでその行で処理を一時停止し、終わったら次の行に進むという動作になります。これにより、複雑な非同期処理も上から下へ順番に実行される普通のプログラムのように書けるため、コードの読みやすさが劇的に向上します。初心者の方は、まず「非同期関数を作るならasyncをつけ、中で待つならawaitを使う」と覚えておけば間違いありません。
4. 文字列やオブジェクトを返す非同期処理
数値だけでなく、名前などの文字列や、複数の情報がまとまったオブジェクトを返す場合も同様です。例えば、ユーザー情報を取得する関数を考えてみましょう。ユーザー名が文字列である場合、戻り値の型はPromise<string>になります。また、名前と年齢が含まれるような複雑なデータ(オブジェクト)の場合は、interface(インターフェース)を使って型を定義するとさらに分かりやすくなります。
インターフェースとは、データの設計図のようなものです。どのような項目が含まれているかをあらかじめ宣言しておくことで、その設計図通りのデータが返ってくることを保証します。これを非同期関数の戻り値に組み合わせることで、大規模な開発でも迷うことなく作業を進められるようになります。
// ユーザー情報の型を定義
interface User {
id: number;
name: string;
}
// オブジェクトを返す非同期関数
async function getUserData(): Promise<User> {
return {
id: 1,
name: "太郎"
};
}
// awaitを使ってスマートにデータを受け取る
async function main() {
const user = await getUserData();
console.log(user.name);
}
main();
実行結果は以下のようになります。
太郎
5. 何も返さない非同期関数の型定義
処理を実行するだけで、特に結果を返さない関数もあります。例えば、データを保存するだけの処理や、画面にメッセージを表示するだけの処理です。通常の関数では何も返さない場合にvoidという型を使いますが、非同期関数の場合はPromise<void>と記述します。
Promise<void>は、「何かデータが返ってくるわけではないけれど、処理自体は無事に終わったことを知らせる」という役割を持っています。これを使わないと、処理が終わったのかどうかを判定することができず、次の動作に移るタイミングが分からなくなってしまいます。一見地味ですが、プログラムの流れを正しく制御するために非常に重要な型です。
// ログを表示するだけの非同期関数
async function displayMessage(msg: string): Promise<void> {
console.log("メッセージを表示します...");
console.log(msg);
}
async function run() {
await displayMessage("こんにちは、TypeScript!");
console.log("表示が完了しました。");
}
run();
実行結果は以下のようになります。
メッセージを表示します...
こんにちは、TypeScript!
表示が完了しました。
6. 非同期処理でのエラーハンドリングと型
非同期処理は常に成功するとは限りません。ネットワークの調子が悪くてデータが取得できなかったり、指定したファイルが見つからなかったりすることもあります。このような失敗を扱うのがエラーハンドリングです。TypeScriptでは、try...catchという構文を使ってエラーを捕まえます。
tryの中に成功するはずの処理を書き、もし失敗した場合はcatchの中の処理が実行されます。非同期関数の戻り値の型は成功時のものを定義しますが、失敗したときのことも考えておく必要があります。エラーが発生した際にプログラムが突然終了してしまわないよう、適切にエラー処理を行うことは、プログラミングにおいて非常に大切です。
7. 複数の非同期処理を同時に扱う方法
一つの処理だけでなく、複数の非同期処理をまとめて行いたい場合もあります。例えば、複数の画像を同時に読み込むようなケースです。この場合、一つずつ順番に待っていると時間がかかってしまいます。そこで使われるのがPromise.allという機能です。
Promise.allを使うと、複数の非同期処理を並行して実行し、すべてが終わるまで待つことができます。このときの戻り値は、それぞれの結果を詰め合わせた配列の形になります。戻り値の型も、配列の形式で定義する必要があります。これにより、効率的で高速なアプリケーションを作成することが可能になります。
// 複数のデータを取得する関数
async function fetchItems(): Promise<string[]> {
const item1 = Promise.resolve("りんご");
const item2 = Promise.resolve("みかん");
const item3 = Promise.resolve("バナナ");
// 全ての処理が終わるのを待って配列で受け取る
const results = await Promise.all([item1, item2, item3]);
return results;
}
async function start() {
const inventory = await fetchItems();
console.log(inventory);
}
start();
実行結果は以下のようになります。
["りんご", "みかん", "バナナ"]
8. 非同期関数の型定義を学ぶメリット
最後に、なぜこのように厳密に型をつけるのかについてお話しします。TypeScriptを使う最大の理由は、バグを未然に防ぐことです。型を決めておくことで、エディタ(プログラムを書くソフト)がリアルタイムで間違いを指摘してくれるようになります。非同期処理は特に間違いが起きやすい場所なので、戻り値の型を意識するだけで、開発のスピードと品質は格段に上がります。
最初はPromise<T>という書き方に戸惑うかもしれませんが、慣れてしまえばこれほど心強い味方はありません。自分が書いたコードが将来の自分や、一緒に働く仲間の助けになります。一歩ずつ、確実に理解を深めていきましょう。