TypeScriptのPromiseとは?API通信の型定義を初心者向けに解説

TypeScriptのPromiseとは?API通信の型定義を初心者向けに解説
少年A
fetchでデータを取ってきたのに、プロパティ名を間違えてて、画面に出すまで気づかなかったよ…
キノ
あるあるだね。じゃあ聞くけど、API通信の関数に Promise<User> って書くと、何が変わると思う?
少年A
うーん、User型のデータが返ってくるって分かる、とか?でも、fetchは「あとで」結果が返ってくるんだよね。そんな値にも型を付けられるの?
キノ
いいところに気づいたね。「あとで返ってくる値」にも型を付けられるし、付けると、さっきのタイプミスも書いている時点で気づけるようになるよ
少年A
えっ、本当に?じゃあ、型を付けないと、どうなっちゃうの?
キノ
それが今日のテーマだよ。fetchの実例で、Promise<T>の書き方から、付けないときの落とし穴まで、順番に見ていこう

TypeScriptのPromise<T>とは

TypeScriptのPromise<T>とは、非同期処理が最終的に返す値の型を指定する書き方です。TypeScript Promiseでは、たとえば Promise<User> と書くと、「あとで User 型の値が返ってくる処理」という意味になります。

API通信の結果に型を付けると、レスポンスの中身をエディタの補完付きで安全に扱えるようになります。この記事では、fetchを使った実例で、Promise<T>の書き方からエラー処理、Reactでの使い方までを整理します。

Promiseは、非同期処理の「結果の入れ物」のようなオブジェクトです。次の3つの状態があります。

状態意味
待機中(pending)処理がまだ終わっていない状態
成功(fulfilled)処理が成功し、値を取り出せる状態。Promise<T> の T は、この値の型です
失敗(rejected)処理が失敗した状態。catch で受け取ります

<T> の部分はジェネリクスです。ジェネリクスの基本がまだ不安な方は、こちらを先にご覧ください。

スポンサーリンク

API通信にPromise<T>の型定義が必要な理由

JavaScriptでは、APIから返ってきたデータに何が入っているのかを、コードから読み取れません。プロパティ名のタイプミス(user.nmae など)も、実行して undefined が出るまで気づけません。

Promise<T>で型を付けると、次のような違いが出ます。

  • 書いている時点でタイプミスに気づける:存在しないプロパティに赤線が出ます
  • 入力補完が効く:user. と打つだけで、使えるプロパティが候補に出ます
  • APIの変更に気づきやすい:型定義を直すと、影響するコードがエラーとして一覧で分かります
スポンサーリンク

基本の書き方:async関数にPromise<User>を付ける

API通信の関数は、async 関数の戻り値に Promise<User> のように型を付けて書きます。

type User = {
  id: number;
  name: string;
  email: string;
};

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`https://api.example.com/users/${id}`);
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  const data: User = await res.json();
  return data;
}

ポイントは3つです。

  • 戻り値は Promise<User> と書く:async 関数は、値を Promise に包んで返します。そのため : User ではなく : Promise<User> と書きます
  • res.ok を確認する:fetch は、404や500のようなエラーのレスポンスでは reject されません(MDN)。res.ok が false のときは、自分で例外を投げます
  • await res.json() の結果に型を付ける:data に User と書くことで、以降は User として扱われます

型の書き方そのものは、こちらの記事で解説しています。

ここでは type で定義していますが、interface でも同じように使えます。違いが気になる方はこちらをどうぞ。

戻り値の型注釈を省略するとどうなるか

戻り値の型注釈を書かないと、どうなるでしょうか。次のコードを見てください。

// 戻り値の型注釈がない
async function fetchUserLoose(id: number) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json(); // 戻り値は Promise<any> になる
}

async function demo() {
  const user = await fetchUserLoose(1);
  console.log(user.nmae); // タイプミスでもエラーにならない
}

res.json() の戻り値は Promise<any> です。そのため、注釈を省略した関数の戻り値も Promise<any> と推論され、user.nmae のようなタイプミスでもエラーになりません。

戻り値に Promise<User> を付けた関数なら、同じタイプミスを書いた時点で赤線が出ます。

async function demo() {
  const user = await fetchUser(1); // Promise<User> を返す関数
  console.log(user.nmae);
  // Property 'nmae' does not exist on type 'User'.
}

型注釈は「宣言」であって「検証」ではない

注意したいのは、const data: User = await res.json() と書いても、実行時にレスポンスの中身が本当に User かどうかは確認されない点です。型は「こういう形で返ってくるはず」という宣言にすぎません。

APIが仕様と違う値を返した場合は、型が合っていても実行時にエラーになることがあります。外部のAPIなど、中身を信頼しきれない場合は、実行時にデータを検証するライブラリ(zodなど)を使うと安全です。any と unknown の違いや、as で型を付ける書き方は、次の記事で解説しています。

Promise<any>とPromise<T>の違い

ここまでの違いを、表にまとめます。

比較項目Promise<any>Promise<User>
型チェック効かない効く
入力補完出ない出る
タイプミスの検出実行して初めて気づく書いている時点で気づく
呼び出し側への影響any が引き継がれ、型チェックが効かない範囲が広がるUser として安全に扱える
向いている場面型が決まっていない試作コード本番で動かすコード

Promise<void>・Promise.allなど、よく使う書き方

API通信では、次のような書き方がよく出てきます。

書き方意味
Promise<void>値を返さない非同期処理(保存、削除など)
Promise<Post[]>配列を返す非同期処理
Promise<User | null>見つからないときは null を返す非同期処理
Promise.all複数の通信を同時に実行し、結果をまとめて受け取る

User | null のようなユニオン型については、こちらで解説しています。

配列の型(Post[])の書き方は、こちらの記事が参考になります。

type Post = {
  id: number;
  title: string;
};

// 値を返さない処理は Promise<void>
async function saveUser(user: User): Promise<void> {
  const res = await fetch("https://api.example.com/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(user),
  });
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
}

// 配列を返す処理は Promise<Post[]>
async function fetchPosts(userId: number): Promise<Post[]> {
  const res = await fetch(`https://api.example.com/users/${userId}/posts`);
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  return res.json();
}

async function loadAll() {
  // 2つの通信を同時に実行。結果は [User, Post[]] として扱える
  const [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]);
  console.log(user.name, posts.length);
}

Promise.all に関数の配列を渡すと、結果は [User, Post[]] のように、順番ごとに型が付いて返ってきます。どちらの通信も終わってから次に進むため、順番に await するより待ち時間が短くなります。

関数の戻り値から、Promiseの中身の型を取り出したいときは Awaited を使います。(TypeScript 4.5以降)

// 関数の戻り値から、Promise の中身の型(User)を取り出す
type Resolved = Awaited<ReturnType<typeof fetchUser>>;

この例では、Resolved は User になります。

エラー処理:try/catchとres.okの確認

非同期処理が失敗した(rejectedになった)ときは、try/catch で受け取ります。fetch は、通信そのものが失敗したときだけ reject されます。404や500のようなレスポンスは、res.ok を確認して、自分で例外を投げる必要があります。

async function showEmail() {
  try {
    const user = await fetchUser(2);
    console.log(user.email);
  } catch (error) {
    // strict では error は unknown 型。そのままでは error.message を読めない
    if (error instanceof Error) {
      console.error(error.message);
    }
  }
}

strict を有効にしていると、catch の error は unknown 型になります。そのため、instanceof Error で絞り込んでから message を読みます。strict の設定は、tsconfig.json で行います。

Reactでの使用例

Reactでは、useEffect の中でAPI通信の関数を呼び、結果を useState に保存する形がよく使われます。このコードは .tsx ファイルに書きます。

import { useEffect, useState } from "react";

function UserCard({ id }: { id: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let ignore = false; // 古いレスポンスで state を上書きしないための目印

    fetchUser(id)
      .then((data) => {
        if (!ignore) setUser(data);
      })
      .catch((e: unknown) => {
        if (!ignore) setError(e instanceof Error ? e.message : "unknown error");
      });

    return () => {
      ignore = true;
    };
  }, [id]);

  if (error) return <p>エラー: {error}</p>;
  if (!user) return <p>読み込み中...</p>;
  return <p>{user.name}</p>;
}
  • useState<User | null>(null) のように、取得前は null を許す型にします
  • useEffect に渡す関数は、直接 async にできません。そのため .then() でつなぐか、中で async 関数を定義して呼びます
  • ignore の目印は、id が切り替わったときに、古いレスポンスで state を上書きしないためのものです。レスポンスは、送った順番どおりに届くとは限りません(React公式ドキュメントでも紹介されています)

なお、React公式ドキュメントでは、フレームワークやデータ取得用のライブラリが提供する機能を使うほうが効率的だと説明されています。まずは仕組みを理解するために、この形を押さえておくと役立ちます。

.tsと.tsxの使い分けは、こちらの記事で解説しています。

よくあるエラーと対処

1. Type ‘Promise<User>’ is missing the following properties from type ‘User’

await を付け忘れたときのエラーです。Promiseのまま、User 型の変数に入れようとしています。

// NG:await を付け忘れた
const user: User = fetchUser(1);
// Type 'Promise<User>' is missing the following properties from type 'User': id, name, email

// OK
const user2: User = await fetchUser(1);

2. Property ‘name’ does not exist on type ‘Promise<User>’

これも await の付け忘れです。Promiseそのものには name がありません。

// NG:Promise のまま .name を読もうとした
const name = fetchUser(1).name;
// Property 'name' does not exist on type 'Promise<User>'.

// OK
const name2 = (await fetchUser(1)).name;

3. ‘await’ expressions are only allowed within async functions

async が付いていない関数の中で await を使ったときのエラーです。関数に async を付けます。

// NG:async のない関数の中で await を使った
function getName() {
  const user = await fetchUser(1);
  // 'await' expressions are only allowed within async functions and at the top levels of modules.
  return user.name;
}

// OK:async を付けて、戻り値を Promise<string> にする
async function getNameFixed(): Promise<string> {
  const user = await fetchUser(1);
  return user.name;
}

よくある質問

Q. Promise<T>のTには何を入れますか?
A. 処理が成功したときに取り出せる値の型を入れます。ユーザー情報を返すなら Promise<User>、値を返さないなら Promise<void> です。

Q. async関数の戻り値に「: User」と書いてもいいですか?
A. 書けません。async 関数の戻り値の型は、Promise<User> のように Promise に包んで書きます。

Q. res.json() の結果は、型チェックされますか?
A. されません。res.json() は Promise<any> を返すため、型を付けても実行時の検証にはなりません。信頼できないAPIの場合は、zodなどで実行時に検証します。

Q. fetchは、404エラーのときにrejectされますか?
A. されません。通信そのものが失敗したときだけ reject されます。404や500のときは、res.ok を確認して、自分で例外を投げます。

Q. thenとasync/awaitは、どちらを使えばいいですか?
A. どちらも同じPromiseを扱う書き方です。普段は読みやすい async/await がおすすめです。Reactの useEffect の中では、直接 async にできないため、.then() を使うか、中に async 関数を定義します。

まとめ:TypeScript Promise とは何かのおさらい

  • Promise<T>は、非同期処理が最終的に返す値の型を指定する書き方
  • async 関数の戻り値は Promise<User> のように書く
  • 戻り値の型注釈を省略すると、res.json() の any が引き継がれ、タイプミスに気づけない
  • 型注釈は実行時の検証ではない。信頼できないAPIは、実行時にも検証する
  • fetch は404でもrejectされないので、res.ok を確認する

公式ドキュメントにも、詳しい解説があります。

少年A
型を付けておくだけで、タイプミスにも気づけるんだね
キノ
うん。まずは、fetchする関数の戻り値に Promise<User> を付けるところから試してみてね

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です