TypeScriptのasync/awaitとPromise型推論を完全攻略!初心者がハマる原因と解決策
生徒
「TypeScriptで非同期処理を作っているのですが、asyncやawaitを使うとデータの種類がうまく判定されなくてエラーになってしまいます。」
先生
「それは型の推論がうまくいっていない証拠ですね。TypeScriptでは、あとから届くデータの中身をあらかじめ教えてあげる必要があるんですよ。」
生徒
「あとから届くデータ、ですか?具体的にどうやって型を指定すればいいのか教えてください!」
先生
「もちろんです!Promiseの仕組みから、型の書き方まで一つずつ丁寧に解説していきますね。」
1. 非同期処理とPromiseの基本を知ろう
プログラミングの世界には、非同期処理という考え方があります。これは、時間がかかる作業を待っている間に、他の作業を先に進めておく仕組みのことです。例えば、レストランで料理を注文したあと、料理ができあがるまで席でスマホを見たり会話をしたりして待つのと同じです。料理が完成するのを何もせずにじっと厨房の前で立ち尽くして待つ必要はありませんよね。
この「あとで料理が届くよ」という約束の状態を、TypeScriptやJavaScriptではPromise(プロミス)と呼びます。Promiseは直訳すると「約束」です。今はまだ手元にデータはないけれど、未来のいつか、成功してデータが返ってくるか、あるいは失敗してエラーが返ってくることを約束するオブジェクトなのです。
TypeScriptはこのPromiseを扱うのが得意ですが、初心者の方は「中身が何であるか」を正しく設定しないと、プログラムが混乱してしまいます。まずはこの約束の概念をしっかり理解することが、型推論をマスターする第一歩となります。
2. asyncとawaitの役割と書き方
非同期処理をより人間が読みやすい形で書くために登場したのが、async(エイシンク)とawait(アウェイト)です。これらはセットで使われることが多い魔法の言葉です。関数の前にasyncをつけると、その関数は自動的にPromiseを返すようになります。そして、その関数の中でawaitを使うと、「データが返ってくるまでここで少し待ってね」という指示になります。
通常、非同期処理は非常に複雑な書き方になりがちですが、これらを使うことで、上から下へ順番に処理が進んでいるかのようにコードを書くことができます。しかし、ここで問題になるのが型推論です。型推論とは、TypeScriptが「この変数は数字だね」「これは文字列だね」と自動で判断してくれる機能のことですが、非同期処理の場合はこの判断が難しくなることがあるのです。
3. 型がうまく推論されない主な原因
なぜTypeScriptは、async関数で型をうまく予測できないことがあるのでしょうか。最大の原因は、Promiseの中身を定義していないことにあります。例えば、箱の中に何かが入っていることはわかっても、開けてみるまでリンゴなのかミカンなのかわからない状態と同じです。TypeScriptは非常に慎重な性格なので、中身がわからないものに対しては「これは何かわかりません(unknownやany)」というラベルを貼ってしまいます。
また、外部のサーバーからデータを取得するAPI通信などでは、どのような形式のデータが返ってくるかをTypeScriptが事前に知る術がありません。そのため、開発者が明示的に「こういう形のデータが返ってくるはずだよ」と教えない限り、推論は途切れてしまうのです。これが原因で、せっかくTypeScriptを使っているのに、コードを書いている途中で便利な入力補完が効かなかったり、エラーが表示されたりすることになります。
4. Promiseに型を指定する具体的な方法
型を正しく推論させるための最も確実な方法は、関数の戻り値に対してジェネリクスを使うことです。ジェネリクスとは、型をパラメータのように扱える仕組みで、Promise<型名>という形式で記述します。これにより、「この約束が果たされたときには、この型のデータが返ってきます」という宣言になります。
それでは、実際にシンプルなコードで確認してみましょう。名前を返すだけの簡単な非同期関数の例です。
async function getUserName(): Promise<string> {
return "たろう";
}
async function main() {
const name = await getUserName();
console.log(name.length);
}
このコードでは、Promise<string>と書くことで、awaitした結果が必ず文字列(string)になることを伝えています。そのおかげで、変数nameに対して文字列のプロパティであるlength(文字数)を安全に使うことができるようになるのです。もし型を指定しなかったら、TypeScriptはnameが何かわからず、エラーを出していたかもしれません。
5. オブジェクトを扱う高度な型推論
実際の開発では、単なる文字列だけでなく、複数のデータが集まったオブジェクトを扱うことが多いでしょう。例えば、ユーザーのID、名前、年齢がセットになったデータなどです。このような場合は、まずinterface(インターフェース)やtype(タイプ)を使って、データの設計図を作成します。設計図を先に作っておくことで、Promiseの中身を詳細に定義できるようになります。
interface User {
id: number;
name: string;
isPremium: boolean;
}
async function fetchUser(): Promise<User> {
// 実際はここでインターネットからデータを取得するイメージです
return {
id: 1,
name: "山田はなこ",
isPremium: true
};
}
async function displayUser() {
const user = await fetchUser();
console.log("ユーザー名: " + user.name);
}
このように、自作したUserという型をPromise<User>として指定することで、user.nameやuser.idといった項目が正しく認識されます。パソコンのキーボードでuser.と打ち込んだ瞬間に、候補としてnameやidが表示されるようになるのは、この型指定のおかげです。これにより、打ち間違いによるミスを劇的に減らすことができます。
6. 配列データの非同期取得と型定義
次に、複数のデータが並んだ配列を取得する場合の対処法を見ていきましょう。ニュース記事のリストや、商品の在庫一覧などを取得する際に使います。配列の場合は、Promise<型名[]>という書き方をします。末尾にブラケット(角括弧)をつけるのがポイントです。
type Product = {
title: string;
price: number;
};
async function getProducts(): Promise<Product[]> {
const items = [
{ title: "ノートパソコン", price: 120000 },
{ title: "マウス", price: 30000 }
];
return items;
}
async function showTotal() {
const products = await getProducts();
products.forEach(p => {
console.log(p.title + "の価格は" + p.price + "円です");
});
}
実行結果は以下のようになります。
ノートパソコンの価格は120000円です
マウスの価格は30000円です
配列の型推論がうまくいっていると、forEachなどの便利な命令を使っている際にも、中身の一つひとつの要素がProduct型であることをTypeScriptが理解してくれます。プログラミング初心者がよくやってしまう「配列なのに単体のデータとして扱ってしまう」といったミスを防ぐ強力な盾になってくれます。
7. 型アサーションを使った最終手段
どうしてもTypeScriptの推論がうまくいかない場合や、外部ライブラリの都合で型が定義されていない場合には、型アサーションという機能を使うことがあります。これは「このデータは絶対にこの型だから信じて!」とプログラマがTypeScriptに強制的に指示を出す方法です。asというキーワードを使います。
async function getUnknownData() {
const response = await fetch("https://example.com/api/data");
const data = await response.json();
// 取得したデータが何かわからないので、無理やり型を教える
const fixedData = data as { message: string };
console.log(fixedData.message);
}
ただし、型アサーションは非常に強力ですが、使いすぎには注意が必要です。実際にはデータの中身が違うのに、無理やり違う型だと嘘をついてしまうと、プログラムが実行中に壊れてしまう原因になるからです。可能な限り、これまでに紹介したPromise<T>の形式で正しく定義することを優先し、どうしても解決できない時の予備手段として覚えておきましょう。初心者のうちは、まずは正攻法である戻り値への型指定を練習することをおすすめします。
8. エラーハンドリングと型の安全性
非同期処理では、常に成功するとは限りません。インターネットが切れていたり、サーバーが故障していたりすることもあります。そのために必要なのがtry-catch(トライ・キャッチ)という仕組みです。TypeScriptでは、エラーが発生した際の「エラーオブジェクト」の型についても考える必要があります。
最新のTypeScriptでは、キャッチされたエラーの型はデフォルトでunknown(不明)となっています。これは安全性を高めるための仕様です。エラーが発生した時に、そのエラーがどのようなメッセージを持っているかを安全に確認するためには、型のチェックが必要になります。これにより、予期せぬエラーで画面が真っ白になってしまうような事態を未然に防ぎ、ユーザーにとって優しいアプリを作ることができるようになります。型を意識することは、単にプログラムを書くのを楽にするだけでなく、最終的に使う人の安心にも繋がっているのです。