はじめてのTypeScriptプログラミング入門 30日コース | Generics・Utility Types・Class・Module - Day 28:非同期処理

TypeScript 30日で身につけるTypeScript
スポンサーリンク
スポンサーリンク

Day 28:非同期処理の世界に入ってみる ― Promise / async / await

第4週のDay 28では、いよいよ 非同期処理 に入っていきます。 WebアプリやAPI通信では、ほぼ必ず登場する重要テーマです。

扱うキーワードは次のとおりです。

  • Promise
  • async
  • await
  • try / catch
  • API通信の基本
  • Promiseの型

実践テーマはこうです。

疑似APIを作る。 async function getUser(): Promise<User> { ... }

「サーバーからデータを取ってくる」イメージを使いながら、 非同期処理の基本をステップバイステップでかみ砕いていきます。

非同期処理とは何かをざっくりイメージする

「すぐ終わらない処理」とどう付き合うか

プログラムの中には、次のような「すぐ終わらない処理」があります。

  • ネットワーク越しにAPIからデータを取得する
  • データベースにアクセスする
  • ファイルを読み書きする

こうした処理は、結果が返ってくるまで時間がかかることが多いです。 その間、プログラム全体が止まってしまうと困るので、

「結果が返ってきたら教えてね」 「それまで他の処理を進めておくね」

というスタイルで付き合う必要があります。 このときに登場するのが Promise と async / await です。

Promiseとは何か ― 「未来の値」の約束

Promiseのざっくりしたイメージ

Promiseは、よくこう説明されます。

「まだ手元にないけれど、将来手に入る“かもしれない”値の約束」

たとえば、

  • 「APIからユーザー情報を取ってくる」関数は、 今すぐ User を返せるわけではありません。
  • 代わりに、「あとで User を返すよ」という Promise\<User\> を返します。

TypeScriptでは、Promiseの型はこう書きます。

Promise<User>  // 「将来Userを返すPromise」という意味
Promise<string> // 「将来stringを返すPromise」
Promise<void>   // 「将来何も返さない(完了だけ知らせる)Promise」
TypeScript

async / awaitとは何か ― Promiseを「同期っぽく」書くための仕組み

async関数の役割

async を関数につけると、その関数は 必ずPromiseを返す関数 になります。

async function example(): Promise<number> {
  return 42; // 実際には Promise<number> として返される
}
TypeScript
  • async を付けることで、 「この関数は非同期処理を含むかもしれないよ」という宣言になります。
  • 関数の戻り値の型は、Promise<...> になります。

awaitの役割

await は、

「Promiseの結果が返ってくるまで待って、その中身を取り出す」

ためのキーワードです。

async function example(): Promise<void> {
  const value = await someAsyncFunction(); // someAsyncFunctionはPromiseを返す
  console.log(value); // Promiseの中身(解決された値)を使える
}
TypeScript
  • await は async 関数の中でしか使えない というルールがあります。
  • await を使うことで、 「非同期処理の結果を、あたかも同期処理のように扱う」書き方ができます。

Step 1:User型を用意する

まずは、疑似APIで返したい User 型を定義します。

// User型の定義
type User = {
  id: number;
  name: string;
  email: string;
};
TypeScript

シンプルに、ID・名前・メールアドレスだけを持つユーザーとします。

Step 2:疑似API関数 getUser を設計する

次に、疑似APIとして getUser 関数を作ります。

要件はこうです。

  • async function getUser(): Promise<User> という形にする
  • 内部で「APIからデータを取ってくるっぽい」処理をする
  • 少し待ってから User を返す(擬似的な遅延)

setTimeoutで「遅延」を再現する

ブラウザやNode.jsでは、setTimeout を使って「一定時間後に処理を実行する」ことができます。 これを使って、疑似的なAPI遅延を再現します。

// 疑似API:ユーザー情報を取得する
async function getUser(): Promise<User> {
  // Promiseを自分で作って、その中でsetTimeoutを使う
  return new Promise<User>((resolve) => {
    // 1秒後にユーザー情報を返す
    setTimeout(() => {
      const user: User = {
        id: 1,
        name: "Taro",
        email: "[email protected]",
      };
      resolve(user); // 成功時の値を返す
    }, 1000);
  });
}
TypeScript

ここが重要ポイントです:

  • new Promise<User>((resolve) => { ... })
    • 「将来Userを返すPromise」を自分で作っています。
  • setTimeout(() => { ... }, 1000);
    • 1秒後に resolve(user) を呼び出すことで、 「1秒待ってからUserが手に入る」Promiseになっています。
  • async function getUser(): Promise<User>
    • async を付けることで、戻り値の型が Promise<User> であることを明示しています。

Step 3:getUserを呼び出してみる(await版)

次に、この getUser を実際に呼び出してみます。 await を使うと、Promiseの結果を「普通の値」のように扱えます。

// getUserを使ってユーザー情報を取得し、表示する関数
async function showUser(): Promise<void> {
  console.log("ユーザー情報を取得中...");

  // getUserの結果が返ってくるまで待つ
  const user = await getUser();

  console.log("取得したユーザー:");
  console.log(`ID: ${user.id}`);
  console.log(`名前: ${user.name}`);
  console.log(`メール: ${user.email}`);
}

// 実行例
showUser();
TypeScript

ポイント:

  • await getUser();
    • getUser() は Promise<User> を返しますが、 await を使うことで「中身の User」を取り出しています。
  • showUser 自体も async 関数なので、戻り値は Promise<void> になります。
    • 「非同期処理を含む関数は、基本的にPromiseを返す」と覚えておくとよいです。

Step 4:エラー処理 ― try / catch で失敗に備える

現実のAPI通信では、常に成功するとは限りません。

  • ネットワークエラー
  • サーバーエラー
  • タイムアウト

など、いろいろな失敗が起こり得ます。 そのため、非同期処理では エラー処理 がとても重要です。

疑似APIに「失敗の可能性」を追加する

まず、getUser を少し改造して、 たまに失敗する疑似APIにしてみます。

// 疑似API:たまに失敗するバージョン
async function getUserWithError(): Promise<User> {
  return new Promise<User>((resolve, reject) => {
    setTimeout(() => {
      const shouldFail = Math.random() < 0.3; // 30%の確率で失敗

      if (shouldFail) {
        // 失敗時はrejectを呼ぶ
        reject(new Error("ユーザー情報の取得に失敗しました"));
        return;
      }

      // 成功時はresolveを呼ぶ
      const user: User = {
        id: 1,
        name: "Taro",
        email: "[email protected]",
      };
      resolve(user);
    }, 1000);
  });
}
TypeScript

ここが重要ポイントです:

  • new Promise<User>((resolve, reject) => { ... })
    • 成功時は resolve(value)、失敗時は reject(error) を呼びます。
  • reject(new Error("..."))
    • これにより、Promiseは「失敗状態」として完了します。
    • await で待っている側では、例外として扱われます。

try / catchでエラーを受け止める

次に、この失敗する可能性のあるAPIを呼び出す側で、 try / catch を使ってエラーを受け止めます。

// エラーを考慮したユーザー表示関数
async function showUserSafely(): Promise<void> {
  console.log("ユーザー情報を取得中...");

  try {
    const user = await getUserWithError(); // ここで失敗する可能性がある
    console.log("取得したユーザー:");
    console.log(`ID: ${user.id}`);
    console.log(`名前: ${user.name}`);
    console.log(`メール: ${user.email}`);
  } catch (error) {
    // 失敗時はここに入ってくる
    console.error("ユーザー情報の取得に失敗しました");
    console.error("詳細:", (error as Error).message);
  }
}

// 実行例
showUserSafely();
TypeScript

重要ポイント:

  • try { ... } catch (error) { ... }
    • await している非同期処理が reject された場合、 そのエラーは catch ブロックで受け止められます。
  • (error as Error).message
    • TypeScriptでは、error の型が unknown になることが多いので、 as Error でキャストしてから message を参照しています。

Step 5:Promiseの型を意識して書いてみる

TypeScriptでは、Promiseの型を明示することで、 「この非同期関数が何を返すのか」をはっきりさせることができます。

基本パターン

  • async function foo(): Promise<number> { ... }
    • 将来 number を返す非同期関数
  • async function bar(): Promise<void> { ... }
    • 将来「完了だけ」を知らせる非同期関数
  • async function baz(): Promise<User[]> { ... }
    • 将来 User の配列を返す非同期関数

getUserの型を改めて確認する

async function getUser(): Promise<User> {
  // ...
}
TypeScript
  • 戻り値の型が Promise<User> であることを明示することで、 呼び出し側は「この関数は非同期でUserを返す」と理解できます。
  • await getUser() と書いたとき、 user の型は User になります(Promiseの中身の型)。

まとめ用テンプレート:Day 28の疑似API一式

最後に、Day 28の内容をまとめた練習用テンプレートを載せます。 このままコピーして、エラー率を変えたり、 User にプロパティを追加したりしながら、 非同期処理の感覚を育ててみてください。

// day28-async-api.ts
// Day 28:非同期処理・疑似API練習

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

// 成功する疑似API
async function getUser(): Promise<User> {
  return new Promise<User>((resolve) => {
    setTimeout(() => {
      const user: User = {
        id: 1,
        name: "Taro",
        email: "[email protected]",
      };
      resolve(user);
    }, 1000); // 1秒後に成功
  });
}

// たまに失敗する疑似API
async function getUserWithError(): Promise<User> {
  return new Promise<User>((resolve, reject) => {
    setTimeout(() => {
      const shouldFail = Math.random() < 0.3; // 30%の確率で失敗

      if (shouldFail) {
        reject(new Error("ユーザー情報の取得に失敗しました"));
        return;
      }

      const user: User = {
        id: 1,
        name: "Taro",
        email: "[email protected]",
      };
      resolve(user);
    }, 1000);
  });
}

// シンプルな表示関数(成功前提)
async function showUser(): Promise<void> {
  console.log("=== showUser ===");
  console.log("ユーザー情報を取得中...");

  const user = await getUser();

  console.log("取得したユーザー:");
  console.log(`ID: ${user.id}`);
  console.log(`名前: ${user.name}`);
  console.log(`メール: ${user.email}`);
}

// エラー対応版表示関数
async function showUserSafely(): Promise<void> {
  console.log("=== showUserSafely ===");
  console.log("ユーザー情報を取得中...");

  try {
    const user = await getUserWithError();
    console.log("取得したユーザー:");
    console.log(`ID: ${user.id}`);
    console.log(`名前: ${user.name}`);
    console.log(`メール: ${user.email}`);
  } catch (error) {
    console.error("ユーザー情報の取得に失敗しました");
    console.error("詳細:", (error as Error).message);
  }
}

// 実行例
showUser().then(() => {
  // showUserが終わったら次を実行
  return showUserSafely();
});
TypeScript

Day 28のまとめ

Day 28では、

  • 非同期処理のイメージ(「すぐ終わらない処理」との付き合い方)
  • Promiseとは何か(「未来の値の約束」)
  • async / await を使って、Promiseを「同期っぽく」扱う書き方
  • try / catch で非同期処理のエラーを受け止める方法
  • async function getUser(): Promise<User> という形で、 疑似APIを設計・実装する流れ

を、具体的なコードとコメント付きで見てきました。

非同期処理は、最初は少しとっつきにくく感じるかもしれませんが、 今日のように「疑似API」を題材にして何度か書いてみると、 だんだんと「時間の流れ」を意識したコードが自然に書けるようになっていきます。

次のステップとして、

  • getUser に「ユーザーIDを引数で渡す」ようにする
  • getUsers(): Promise<User[]> のように、配列を返すAPIを作る
  • 複数のAPIを await で順番に呼び出す

などを試してみると、 非同期処理の世界がさらに立体的に見えてくるはずです。


Day 28:非同期処理演習 ― 成功・エラー・Loadingを全部扱ってみる

Day 28の演習テーマは、

非同期処理 成功処理 エラー処理 Loading状態 を実装する。

というものです。 ここまで学んだ Promise / async / await / try / catch / Promiseの型 を使って、 「API通信の基本パターン」を小さなサンプルで体験していきます。

ここでは、ユーザー情報を取得する疑似APIを題材にして、

  • 非同期でデータを取りに行く
  • その間は「Loading状態」
  • 成功したら「成功状態」
  • 失敗したら「エラー状態」

という、よくある流れをステップバイステップでかみ砕いていきます。

状態を3つに分けて考える:loading / success / error

まず、非同期処理のときに意識したい状態は、ざっくり次の3つです。

  • Loading状態:
    • 「今まさにデータを取りに行っているところ」
    • 画面では「読み込み中…」などを表示するイメージです。
  • Success状態:
    • 「データ取得に成功して、結果が手元にある状態」
    • 画面では、取得したユーザー情報などを表示します。
  • Error状態:
    • 「何かの理由でデータ取得に失敗した状態」
    • 画面では、「エラーが発生しました」などのメッセージを表示します。

この3つをコードの中でどう表現するか、というのが今回の演習のポイントです。

Step 1:User型と状態管理用の型を用意する

まずは、ユーザー情報と状態を表す型を定義します。

// ユーザー情報の型
type User = {
  id: number;
  name: string;
  email: string;
};

// 非同期処理の状態を表す型
type AsyncState<T> = {
  loading: boolean; // 読み込み中かどうか
  data: T | null;   // 成功時のデータ(まだない場合はnull)
  error: string | null; // エラーメッセージ(エラーがない場合はnull)
};
TypeScript

ポイント:

  • AsyncState<T> はジェネリック型で、 「どんなデータ型でも同じパターンで状態管理できる」ようにしています。
  • loading / data / error の3つを持つことで、 成功・失敗・読み込み中をひとまとめに扱えます。

Step 2:疑似APIを作る ― 成功も失敗もあり得る

次に、ユーザー情報を取得する疑似APIを作ります。 ここでは、たまに失敗するようにして、エラー処理も試せるようにします。

// 疑似API:ユーザー情報を取得する(たまに失敗する)
async function fetchUser(): Promise<User> {
  return new Promise<User>((resolve, reject) => {
    console.log("API呼び出し開始...");

    setTimeout(() => {
      const shouldFail = Math.random() < 0.3; // 30%の確率で失敗

      if (shouldFail) {
        // 失敗時はrejectを呼ぶ
        reject(new Error("ユーザー情報の取得に失敗しました"));
        return;
      }

      // 成功時はresolveを呼ぶ
      const user: User = {
        id: 1,
        name: "Taro",
        email: "[email protected]",
      };
      resolve(user);
    }, 1000); // 1秒後に結果を返す
  });
}
TypeScript

重要ポイント:

  • Promise<User> を自分で作り、成功時は resolve(user)、失敗時は reject(error) を呼んでいます。
  • async function fetchUser(): Promise<User> とすることで、 「この関数は非同期でUserを返す」ということが型からも分かります。

Step 3:状態を管理しながら非同期処理を実行する関数を書く

次に、AsyncState<User> を使って、

  • Loading状態にする
  • 成功したらdataにUserを入れる
  • 失敗したらerrorにメッセージを入れる

という流れをひとつの関数にまとめます。

// ユーザー取得の状態を管理する関数
async function loadUser(): Promise<AsyncState<User>> {
  // まずはLoading状態からスタート
  let state: AsyncState<User> = {
    loading: true,
    data: null,
    error: null,
  };

  console.log("状態: loading =", state.loading);

  try {
    // 非同期API呼び出し(ここで成功・失敗が決まる)
    const user = await fetchUser();

    // 成功したら、loadingをfalseにしてdataにユーザーをセット
    state = {
      loading: false,
      data: user,
      error: null,
    };

    console.log("状態更新: 成功");
    return state;
  } catch (error) {
    // 失敗したら、loadingをfalseにしてerrorにメッセージをセット
    state = {
      loading: false,
      data: null,
      error: (error as Error).message,
    };

    console.log("状態更新: エラー");
    return state;
  }
}
TypeScript

ここが今回の演習の核心です:

  • loading を最初に true にして、「今読み込み中だよ」と明示しています。
  • await fetchUser() が成功したら、data にユーザー情報を入れ、error は null にします。
  • 失敗したら、error にメッセージを入れ、data は null のままにします。
  • どちらの場合も、最後は loading: false にして、「読み込みは終わった」状態にします。

Step 4:状態に応じて表示を切り替える

次に、loadUser() の結果を使って、 状態に応じて表示を切り替える処理を書いてみます。

// 状態に応じて表示を切り替える関数
async function showUserWithState(): Promise<void> {
  console.log("=== ユーザー情報取得処理開始 ===");

  // 非同期処理+状態管理
  const state = await loadUser();

  console.log("=== 結果表示 ===");

  if (state.loading) {
    // 本来ここには来ない想定ですが、念のため
    console.log("読み込み中です...");
    return;
  }

  if (state.error) {
    // エラー状態
    console.log("エラーが発生しました:");
    console.log(state.error);
    return;
  }

  if (state.data) {
    // 成功状態
    console.log("ユーザー情報の取得に成功しました:");
    console.log(`ID: ${state.data.id}`);
    console.log(`名前: ${state.data.name}`);
    console.log(`メール: ${state.data.email}`);
    return;
  }

  // ここに来るのはかなり例外的ですが、念のため
  console.log("予期しない状態です(dataもerrorもありません)");
}

// 実行例
showUserWithState();
TypeScript

ポイント:

  • state.loading / state.error / state.data を順番にチェックすることで、 「今どの状態なのか」を分岐しています。
  • 実際の画面(UI)では、
    • loading === true → スピナーや「読み込み中…」表示
    • error !== null → エラーメッセージ表示
    • data !== null → 実データ表示 という形で使われることが多いです。

Step 5:Loading状態をもう少し意識してみる

コンソールだけだと少し分かりにくいので、 Loading状態をログで強調してみます。

async function showUserWithLoadingLog(): Promise<void> {
  console.log("=== ユーザー情報取得(Loading状態付き) ===");

  // Loading開始
  console.log("Loading... ユーザー情報を取得しています。");

  try {
    const user = await fetchUser(); // ここで1秒ほど待つ

    console.log("Loading完了(成功)");
    console.log("取得したユーザー:");
    console.log(`ID: ${user.id}`);
    console.log(`名前: ${user.name}`);
    console.log(`メール: ${user.email}`);
  } catch (error) {
    console.log("Loading完了(エラー)");
    console.error("ユーザー情報の取得に失敗しました");
    console.error("詳細:", (error as Error).message);
  }
}

// 実行例
showUserWithLoadingLog();
TypeScript

ここで意識したいこと:

  • 「Loading中」と「Loading完了(成功/エラー)」をログで分けて出すことで、 非同期処理の時間の流れがイメージしやすくなります。
  • UIでは、この「Loading中」のタイミングでスピナーを表示し、 完了したらスピナーを消して結果を表示する、という形になります。

まとめ用テンプレート:Day 28演習一式

最後に、今回の演習内容をひとつのファイルにまとめたテンプレートを載せます。 このままコピーして、エラー率を変えたり、 User にプロパティを追加したりしながら、 非同期処理+状態管理の感覚を育ててみてください。

// day28-async-state.ts
// Day 28:非同期処理演習(成功・エラー・Loading)

// ユーザー情報の型
type User = {
  id: number;
  name: string;
  email: string;
};

// 非同期処理の状態を表すジェネリック型
type AsyncState<T> = {
  loading: boolean;
  data: T | null;
  error: string | null;
};

// 疑似API:ユーザー情報を取得する(たまに失敗する)
async function fetchUser(): Promise<User> {
  return new Promise<User>((resolve, reject) => {
    console.log("API呼び出し開始...");

    setTimeout(() => {
      const shouldFail = Math.random() < 0.3; // 30%の確率で失敗

      if (shouldFail) {
        reject(new Error("ユーザー情報の取得に失敗しました"));
        return;
      }

      const user: User = {
        id: 1,
        name: "Taro",
        email: "[email protected]",
      };
      resolve(user);
    }, 1000);
  });
}

// 状態管理付きのユーザー取得関数
async function loadUser(): Promise<AsyncState<User>> {
  let state: AsyncState<User> = {
    loading: true,
    data: null,
    error: null,
  };

  console.log("状態: loading =", state.loading);

  try {
    const user = await fetchUser();

    state = {
      loading: false,
      data: user,
      error: null,
    };

    console.log("状態更新: 成功");
    return state;
  } catch (error) {
    state = {
      loading: false,
      data: null,
      error: (error as Error).message,
    };

    console.log("状態更新: エラー");
    return state;
  }
}

// 状態に応じて表示を切り替える関数
async function showUserWithState(): Promise<void> {
  console.log("=== ユーザー情報取得処理開始 ===");

  const state = await loadUser();

  console.log("=== 結果表示 ===");

  if (state.loading) {
    console.log("読み込み中です...");
    return;
  }

  if (state.error) {
    console.log("エラーが発生しました:");
    console.log(state.error);
    return;
  }

  if (state.data) {
    console.log("ユーザー情報の取得に成功しました:");
    console.log(`ID: ${state.data.id}`);
    console.log(`名前: ${state.data.name}`);
    console.log(`メール: ${state.data.email}`);
    return;
  }

  console.log("予期しない状態です(dataもerrorもありません)");
}

// Loadingログ付きの表示関数
async function showUserWithLoadingLog(): Promise<void> {
  console.log("=== ユーザー情報取得(Loading状態付き) ===");

  console.log("Loading... ユーザー情報を取得しています。");

  try {
    const user = await fetchUser();

    console.log("Loading完了(成功)");
    console.log("取得したユーザー:");
    console.log(`ID: ${user.id}`);
    console.log(`名前: ${user.name}`);
    console.log(`メール: ${user.email}`);
  } catch (error) {
    console.log("Loading完了(エラー)");
    console.error("ユーザー情報の取得に失敗しました");
    console.error("詳細:", (error as Error).message);
  }
}

// 実行例
showUserWithState().then(() => {
  return showUserWithLoadingLog();
});
TypeScript

Day 28演習のまとめ

今回の演習では、

  • 非同期処理の3つの状態(Loading / Success / Error)を意識した設計
  • Promise<T> として非同期関数の戻り値の型を明示すること
  • async / await を使って、非同期処理を「読みやすい形」で書く方法
  • try / catch でエラーを受け止め、状態に反映する流れ

を、疑似APIと状態管理のコードを通して体験しました。

実際のフロントエンド開発では、 この「Loading・成功・エラー」の3状態をどう扱うかが、 ユーザー体験の質を大きく左右します。

今日のテンプレートをベースに、

  • AsyncState<T> を別ファイルに切り出して再利用する
  • fetchUser を fetchUsers(配列)にして一覧表示を試す
  • エラー時にリトライボタンを出すイメージで処理を追加する

など、少しずつ発展させていくと、 非同期処理と状態管理の感覚がぐっと深まっていくはずです。

タイトルとURLをコピーしました