非同期プログラミングは、非同期的に実行されるコードを記述することを可能にするプログラミングパラダイムです。同期プログラミングとは対照的に、コードを順次実行するのではなく、非同期プログラミングではコードをバックグラウンドで実行させながらプログラムの残りの部分が継続して実行されることができます。これは、リモートAPIからデータを取得するなど、完了に時間がかかる可能性のあるタスクに特に有用です。

非同期プログラミングは、JavaScriptで応答性の高い効率的なアプリケーションを作成するために不可欠です。JavaScriptのスーパーセットであるTypeScriptを使用すると、非同期プログラミングをより簡単に扱うことができます。

TypeScriptにおける非同期プログラミングのアプローチには、promisesasync/awaitcallbacksなどがあります。これらのアプローチを詳細に説明し、ユースケースに最適なものを選択できるようにします。

目次

  1. 非同期プログラミングの重要性はなぜですか?

  2. TypeScriptが非同期プログラミングをどのように容易にしますか?

  3. TypeScriptでPromiseを使用する方法

  4. TypeScriptでAsync / Awaitを使用する方法

  5. TypeScriptでコールバックを使用する方法

  6. 結論

なぜ非同期プログラミングが重要なのか?

非同期プログラミングは、応答性と効率の高いWebアプリケーションを構築する際に重要です。これにより、タスクがバックグラウンドで実行される一方で、プログラムの残りの部分が続行され、ユーザーインターフェイスが入力に対して応答できます。また、非同期プログラミングによって複数のタスクを同時に実行することで、全体的なパフォーマンスを向上させることができます。

ユーザーカメラやマイクへのアクセスやユーザー入力イベントの処理など、非同期プログラミングの実世界の例は多数あります。非同期関数を頻繁に作成しなくても、アプリケーションが信頼性があり、適切に機能するようにするために正しく使用する方法を知っておくことが重要です。

TypeScriptが非同期プログラミングを容易にする方法

TypeScriptには、型安全性型推論型チェック、および型注釈など、非同期プログラミングを簡素化するいくつかの機能があります。

型安全性を使用すると、非同期関数を扱う際にもコードが期待どおりに動作することを保証できます。たとえば、TypeScriptは、nullやundefined値に関連するエラーをコンパイル時にキャッチすることができ、デバッグにかかる時間と労力を節約できます。

TypeScriptの型推論とチェックにより、書かなければならない冗長なコードの量が減り、コードがより簡潔で読みやすくなります。

さらに、TypeScriptの型注釈は、非同期関数を理解しにくい場合に特に役立ち、コードの明確さとドキュメント化を提供します。

今、私たちは非同期プログラミングの3つの主要な機能、約束、async/await、およびコールバックについて学んでみましょう。

TypeScriptで約束を使用する方法

約束は、TypeScriptで非同期操作を処理する強力なツールです。たとえば、外部APIからデータを取得したり、メインスレッドが実行されている間にバックグラウンドで時間のかかるタスクを実行したりする場合に、約束を使用するかもしれません。

約束を使用するには、Promiseクラスの新しいインスタンスを作成し、非同期操作を実行する関数を渡します。この関数は、操作が成功した場合に結果を持つresolveメソッドを呼び出すか、失敗した場合にはエラーを持つrejectメソッドを呼び出す必要があります。

約束が作成されたら、thenメソッドを使用してそれにコールバックをアタッチできます。これらのコールバックは、約束が実行されたときにトリガーされ、解決された値がパラメータとして渡されます。約束が拒否された場合、理由を伴う拒否のエラーハンドラをアタッチすることができ、catchメソッドが呼び出されます。

約束を使用すると、従来のコールバックベースの方法よりもいくつかの利点があります。たとえば、約束は、非同期コードでネストされたコールバックが読みにくくなり、メンテナンスが困難になる「コールバック地獄」を防ぐのに役立ちます。

約束を使用すると、非同期コードのエラー処理が簡単になり、catchメソッドを使用して約束チェーン内のどこでエラーが発生してもエラーを管理できます。

最後に、約束は、基礎となる実装に関係なく非同期操作を処理する一貫した、合成可能な方法を提供することで、コードを単純化できます。

Promiseの作成方法

Promise構文:

const myPromise = new Promise((resolve, reject) => {
  // 非同期操作を行う
  // 操作が成功した場合は、結果とともにresolveを呼び出す
  // 操作が失敗した場合は、エラーオブジェクトとともにrejectを呼び出す
});

myPromise
  .then((result) => {
    // 成功した結果を処理する
  })
  .catch((error) => {
    // エラーを処理する
  });
// Promiseを作成する方法の例1

function myAsyncFunction(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    // いくつかの非同期操作
    setTimeout(() => {
      // 成功した操作はpromiseを解決します。 TypeScriptで非同期プログラミングをマスターする最新のブログ投稿をチェックしてください! Promise、Async/Await、およびコールバックを使用して効率的かつスケーラブルなコードを書く方法を学びます。 TypeScriptスキルを次のレベルに引き上げる準備をしましょう!
      const success = true;

      if (success) {
        // 操作が成功した場合は、操作結果でpromiseを解決します
        resolve(
          `The result is success and your operation result is ${operationResult}`
        );
      } else {
        const rejectCode: number = 404;
        const rejectMessage: string = `The result is failed and your operation result is ${rejectCode}`;
        // 操作が失敗した場合は、操作結果でpromiseを拒否します
        reject(new Error(rejectMessage));
      }
    }, 2000);
  });
}

// Promiseを使用する
myAsyncFunction()
  .then((result) => {
    console.log(result); // 出力: 結果は成功で、操作結果は4です
  })
  .catch((error) => {
    console.error(error); // 出力: 結果は失敗で、操作結果は404です
  });

上記の例では、myAsyncFunction()という関数があり、promiseを返します。promiseを作成するためにPromiseコンストラクタを使用し、resolvereject引数を持つcallback functionを取ります。非同期操作が成功した場合は、resolve関数を呼び出します。失敗した場合は、reject関数を呼び出します。

コンストラクタによって返されたpromiseオブジェクトには、成功および失敗のコールバック関数を取るthen()メソッドがあります。promiseが成功した場合は、成功のコールバック関数が結果とともに呼び出されます。promiseが拒否された場合は、失敗のコールバック関数がエラーメッセージとともに呼び出されます。

promiseオブジェクトには、promiseチェーンで発生するエラーを処理するために使用されるcatch()メソッドもあります。catch()メソッドは、promiseチェーンでエラーが発生した場合に呼び出されるコールバック関数を取ります。

それでは、TypeScriptでpromiseをチェーンする方法に移りましょう。

Promiseのチェーン方法

Promiseをチェーンすることで、非同期操作を順次または並行して実行できます。複数の非同期タスクを連続してまたは同時に実行する必要がある場合に役立ちます。たとえば、データを非同期で取得してから非同期で処理する必要があるかもしれません。

promiseをチェーンする方法の例を見てみましょう。

// プロミスをチェーンする方法の例
// 最初のプロミス
const promise1 = new Promise((resolve, reject) => {
  const functionOne: string = "This is the first promise function";
  setTimeout(() => {
    resolve(functionOne);
  }, 1000);
});

// 2番目のプロミス
const promise2 = (data: number) => {
  const functionTwo: string = "This is the second second promise  function";
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(` ${data}  '+'  ${functionTwo} `);
    }, 1000);
  });
};

// 最初と2番目のプロミスを連結する
promise1
  .then(promise2)
  .then((result) => {
    console.log(result); // 出力: これは最初のプロミス関数です + これは2番目のプロミス関数です
  })
  .catch((error) => {
    console.error(error);
  });

上記の例では、2つのプロミスがあります: promise1promise2promise1 は1秒後に文字列 “これは最初のプロミス関数です” で解決されます。 promise2 は数値を入力として受け取り、その数値と文字列 “これは2番目のプロミス関数です” を組み合わせた文字列で解決されるプロミスを返します。

これらの2つのプロミスを then メソッドを使用して連結します。出力の promise1promise2 に入力として渡されます。最後に、再度 then メソッドを使用して promise2 の出力をコンソールにログ出力します。 promise1 または promise2 のいずれかが拒否された場合、エラーは catch メソッドでキャッチされます。

おめでとうございます!TypeScriptでプロミスを作成して連結する方法を学びました。これで、TypeScriptで非同期操作を実行するためにプロミスを使用できるようになりました。次に、TypeScriptで Async/Await がどのように機能するかを見てみましょう。

TypeScriptでの Async / Await の使用方法

Async/awaitは、Promisesとの作業を容易にするためにES2017で導入された構文です。これにより、同期的なコードのように見える非同期コードを記述することができます。

TypeScriptでは、asyncキーワードを使用して非同期関数を定義できます。これにより、コンパイラに関数が非同期であることを伝え、Promiseを返すことができます。

さて、TypeScriptでasync/awaitをどのように使用するかを見てみましょう。

Async / Awaitの構文:

// TypeScriptでのAsync / Await構文
async function functionName(): Promise<ReturnType> {
  try {
    const result = await promise;
    // promiseが解決された後に実行するコード
    return result;
  } catch (error) {
    // promiseがrejectされた場合に実行するコード
    throw error;
  }
}

上記の例では、functionNameはPromiseを返す非同期関数です。 awaitキーワードは、次のコード行に進む前にPromiseが解決するのを待つために使用されます。

try/catchブロックは、非同期関数内で発生したエラーを処理するために使用されます。エラーが発生した場合、catchブロックでキャッチされ、適切に処理できます。

Arrow FunctionsとAsync / Awaitの使用

TypeScriptでは、arrow関数をasync/await構文と一緒に使用することもできます:

const functionName = async (): Promise<ReturnType> => {
  try {
    const result = await promise;
    // promiseが解決された後に実行するコード
    return result;
  } catch (error) {
    // promiseがrejectされた場合に実行するコード
    throw error;
  }
};

上記の例では、functionName は、ReturnType のPromiseを返すアロー関数として定義されています。asyncキーワードはこれが非同期関数であることを示し、awaitキーワードは次の行のコードに進む前にPromiseの解決を待つために使用されます。

APIコールとのAsync / Await

さて、構文を超えて進んで、async/awaitを使ってAPIからデータを取得しましょう。

interface User {
  id: number;
  name: string;
  email: string;
}

const fetchApi = async (): Promise<void> => {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");

    if (!response.ok) {
      throw new Error(
        `Failed to fetch users (HTTP status code: ${response.status})`
      );
    }

    const data: User[] = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
    throw error;
  }
};

fetchApi();

ここでは、JSONPlaceholder APIからデータを取得し、JSONに変換してからコンソールにログを出力しています。これはTypeScriptでasync/awaitを使用する実践的な例です。

コンソールにユーザー情報が表示されるはずです。この画像は出力を示しています:

Axios APIコールとのAsync/Await

// TypeScriptでasync / awaitを使用する方法の例2

const fetchApi = async (): Promise<void> => {
  try {
    const response = await axios.get(
      "https://jsonplaceholder.typicode.com/users"
    );
    const data = await response.data;
    console.log(data);
  } catch (error) {
    console.error(error);
  }
};

fetchApi();

上記の例では、fetchApi() 関数をasync/awaitを使用して定義し、Axios.get() メソッドを使用して指定されたURLにHTTP GETリクエストを行います。awaitを使用して応答を待ち、応答オブジェクトのdataプロパティを使用してデータを取得します。最後に、console.log() でデータをコンソールにログ出力します。発生したエラーはキャッチされ、console.error() でコンソールにログ出力されます。

Axiosを使用してこれを実現することができるため、コンソールに同じ結果が表示されるはずです。

この画像は、Axiosを使用した場合のコンソール出力を示しています:

注意: 上記のコードを試す前に、npmまたはyarnを使用してAxiosをインストールする必要があります。


npm install axios

yarn add axios

Axios に慣れていない場合は、こちらで詳細を学ぶことができます。

エラーを処理するために trycatch ブロックを使用していることがわかります。 trycatch ブロックは、TypeScript でエラーを管理するための手法です。したがって、私たちが行ったような API コールを行う場合は、エラーを処理するために必ず trycatch ブロックを使用してください。

さて、TypeScript で trycatch ブロックをさらに高度に使用する方法を探ってみましょう:

// TypeScript で async / await を使用する方法の例 3

interface Recipe {
  id: number;
  name: string;
  ingredients: string[];
  instructions: string[];
  prepTimeMinutes: number;
  cookTimeMinutes: number;
  servings: number;
  difficulty: string;
  cuisine: string;
  caloriesPerServing: number;
  tags: string[];
  userId: number;
  image: string;
  rating: number;
  reviewCount: number;
  mealType: string[];
}

const fetchRecipes = async (): Promise<Recipe[] | string> => {
  const api = "https://dummyjson.com/recipes";
  try {
    const response = await fetch(api);

    if (!response.ok) {
      throw new Error(`Failed to fetch recipes: ${response.statusText}`);
    }

    const { recipes } = await response.json();
    return recipes; // レシピ配列を返す
  } catch (error) {
    console.error("Error fetching recipes:", error);
    if (error instanceof Error) {
      return error.message;
    }
    return "An unknown error occurred.";
  }
};

// レシピをフェッチしてログに記録
fetchRecipes().then((data) => {
  if (Array.isArray(data)) {
    console.log("Recipes fetched successfully:", data);
  } else {
    console.error("Error message:", data);
  }
});

上記の例では、API から期待されるデータの構造を示す interface Recipe を定義します。その後、指定された API エンドポイントに HTTP GET リクエストを行うために、async/await と fetch() メソッドを使用して fetchRecipes() 関数を作成します。

API リクエスト中に発生する可能性があるエラーを処理するために try/catch ブロックを使用しています。リクエストが成功した場合は、応答からデータプロパティを取得し、それを返します。エラーが発生した場合は、エラーメッセージをチェックし、存在する場合は文字列として返します。

最後に、fetchRecipes() 関数を呼び出し、.then() を使用して返されたデータをコンソールにログ出力します。この例では、async/awaittry/catch ブロックと組み合わせて使用し、より高度なシナリオでエラーを処理する方法を示しています。ここでは、レスポンスオブジェクトからデータを抽出し、カスタムエラーメッセージを返す必要がある場合の手法が示されています。

この画像は、コードの出力結果を示しています:

Promise.all を使った Async / Await

Promise.all() は、入力として約束の配列(反復可能なもの)を取り、単一の Promise を出力します。この Promise は、すべての入力約束が解決されたとき、または入力の反復可能なものに約束が含まれていない場合に解決されます。入力の約束のいずれかが拒否された場合や、非約束がエラーをスローした場合にはすぐに拒否され、最初の拒否メッセージまたはエラーで拒否されます。

// Promise.all を使った async / await の使用例
interface User {
  id: number;
  name: string;
  email: string;
  profilePicture: string;
}

interface Post {
  id: number;
  title: string;
  body: string;
}

interface Comment {
  id: number;
  postId: number;
  name: string;
  email: string;
  body: string;
}

const fetchApi = async <T>(url: string): Promise<T> => {
  try {
    const response = await fetch(url);
    if (response.ok) {
      const data = await response.json();
      return data;
    } else {
      throw new Error(`Network response was not ok for ${url}`);
    }
  } catch (error) {
    console.error(error);
    throw new Error(`Error fetching data from ${url}`);
  }
};

const fetchAllApis = async (): Promise<[User[], Post[], Comment[]]> => {
  try {
    const [users, posts, comments] = await Promise.all([
      fetchApi<User[]>("https://jsonplaceholder.typicode.com/users"),
      fetchApi<Post[]>("https://jsonplaceholder.typicode.com/posts"),
      fetchApi<Comment[]>("https://jsonplaceholder.typicode.com/comments"),
    ]);
    return [users, posts, comments];
  } catch (error) {
    console.error(error);
    throw new Error("Error fetching data from one or more APIs");
  }
};

fetchAllApis()
  .then(([users, posts, comments]) => {
    console.log("Users: ", users);
    console.log("Posts: ", posts);
    console.log("Comments: ", comments);
  })
  .catch((error) => console.error(error));

上記のコードでは、Promise.all を使用して複数のAPIを同時に取得しました。複数のAPIを取得する必要がある場合は、Promise.all を使用して一度にすべてを取得できます。配列のAPIをループ処理するために map を使用し、それを Promise.all に渡して同時に取得しました。

以下の画像は、API呼び出しの出力を示しています:

Promise.all を Axios と一緒に使用する方法を見てみましょう:

// axios と Promise.all を使用した async / await の使用例

const fetchApi = async () => {
  try {
    const urls = [
      "https://jsonplaceholder.typicode.com/users",
      "https://jsonplaceholder.typicode.com/posts",
    ];
    const responses = await Promise.all(urls.map((url) => axios.get(url)));
    const data = await Promise.all(responses.map((response) => response.data));
    console.log(data);
  } catch (error) {
    console.error(error);
  }
};

fetchApi();

上記の例では、Promise.allを使用して同時に2つの異なるURLからデータを取得しています。まず、URLの配列を作成し、axios.getの呼び出しからPromiseの配列を作成するためにmapを使用します。この配列をPromise.allに渡すと、レスポンスの配列が返されます。最後に、再度mapを使用して各レスポンスからデータを取得し、それをコンソールにログ出力します。

Typescriptでコールバックを使用する方法

コールバックは、別の関数に引数として渡される関数です。コールバック関数は、他の関数内で実行されます。コールバックは、関数がタスクが完了する前に実行されないようにし、タスクが完了した直後に実行されるようにします。非同期のJavaScriptコードを記述し、問題やエラーを防ぐのに役立ちます。

// Typescriptでコールバックを使用する例

const add = (a: number, b: number, callback: (result: number) => void) => {
  const result = a + b;
  callback(result);
};

add(10, 20, (result) => {
  console.log(result);
});

以下の画像は、コールバック関数を示しています。

Typescriptでコールバックを使用する別の例を見てみましょう。

// Typescriptでコールバック関数を使用する例

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

const fetchUserData = (
  id: number,
  callback: (error: Error | null, user: User | null) => void
) => {
  const api = `https://jsonplaceholder.typicode.com/users/${id}`;
  fetch(api)
    .then((response) => {
      if (response.ok) {
        return response.json();
      } else {
        throw new Error("Network response was not ok.");
      }
    })
    .then((data) => {
      const user: User = {
        name: data.name,
        email: data.email,
      };
      callback(null, user);
    })
    .catch((error) => {
      callback(error, null);
    });
};

// コールバック関数を使用したfetchUserDataの使用例
fetchUserData(1, (error, user) => {
  if (error) {
    console.error(error);
  } else {
    console.log(user);
  }
});

上記の例では、fetchUserDataという関数があり、idcallbackをパラメーターとして受け取ります。このcallbackは、エラーとユーザーの2つのパラメーターを持つ関数です。

fetchUserData関数は、idを使用してJSONPlaceholder APIエンドポイントからユーザーデータを取得します。フェッチが成功すると、Userオブジェクトを作成し、エラーをnullでコールバック関数に渡します。フェッチ中にエラーが発生した場合は、エラーをnullユーザーでコールバック関数に送信します。

fetchUserData関数をコールバックとともに使用するには、idとコールバック関数を引数として提供します。コールバック関数はエラーをチェックし、エラーがない場合はユーザーデータをログに記録します。

以下の画像はAPI呼び出しの出力を示しています:

コールバックの責任ある使用方法

コールバックはTypeScriptの非同期プログラミングにおいて基本的ですが、読みやすく保守しやすい、ピラミッド状に深く入れ子になったコード(”コールバック地獄”)を避けるために注意深く管理する必要があります。効果的にコールバックを使用する方法は次の通りです:

  1. 深い入れ子を避ける

    • 複雑な操作を名前付き関数に分割してコード構造を平坦化する

    • 複雑な非同期ワークフローにはプロミスまたはasync/awaitを使用する(以下で詳細)

  2. エラーハンドリングを最初に

    • 常に(error, result)パラメータのNode.js慣例に従うこと

    • ネストされたコールバックの各レベルでエラーをチェックすること

    function processData(input: string, callback: (err: Error | null, result?: string) => void) {
      // ...常にエラーを最初にcallbackを呼び出す
    }
  1. 型注釈を使用する

    • TypeScriptの型システムを利用してコールバックのシグネチャを強制する

    • コールバックパラメータの明確なインターフェースを定義する

    type ApiCallback = (error: Error | null, data?: ApiResponse) => void;
  1. 制御フローライブラリを検討する
    複雑な非同期操作には、async.jsなどのユーティリティを使用すること:

    • 同時実行

    • シリーズ実行

    • エラーハンドリングパイプライン

コールバックと代替手段の使い分け

コールバックが最適な選択肢である場合と、そうでない場合があります。

コールバックは、非同期操作(単一の完了)、古いライブラリやコールバックを期待するAPIとのインターフェース、イベントリスナーの処理(クリックリスナーやウェブソケットイベントなど)、または単純な非同期処理が必要な軽量ユーティリティを作成する場合に役立ちます。

他のシナリオでは、クリアな非同期フローを持つ保守可能なコードの記述に焦点を当てる必要がある場合、コールバックは問題を引き起こし、代わりにプロミスまたはasync-awaitを選択すべきです。たとえば、複数の操作をチェーンする必要がある場合、複雑なエラー伝播を処理する必要がある場合、モダンなAPI(Fetch APIやFS Promisesなど)を使用する必要がある場合、または並行実行のためにpromise.all()を使用する場合などです。

コールバックからプロミスへの移行の例:

// コールバックバージョン
function fetchUser(id: number, callback: (err: Error | null, user?: User) => void) {
  // ... 
}

// Promiseバージョン
async function fetchUserAsync(id: number): Promise<User> {
  // ...
}

// async/awaitを使用する
try {
  const user = await fetchUserAsync(1);
} catch (error) {
  // エラー処理
}

非同期パターンの進化

パターン 利点 欠点
コールバック シンプルで汎用的 入れ子の複雑さ
プロミス チェーン可能、エラーフローが向上 .then() チェーンが必要
Async/Await 同期的な読みやすさ トランスパイルが必要

現代のTypeScriptプロジェクトでは、イベント駆動型パターンにはコールバックを、複雑な非同期ロジックにはプロミス/async-awaitを使用することが一般的です。重要なのは、特定のユースケースに適したツールを選択し、コードの明瞭さを保つことです。

結論

この記事では、TypeScriptで非同期コードを処理するさまざまな方法について学びました。コールバック、プロミス、async/awaitについて学び、TypeScriptでそれらを使用する方法についても学びました。また、この概念についても学びました。

プログラミングや優れたソフトウェアエンジニアになる方法について詳しく学びたい場合は、私のYouTubeチャンネル「CliffTech」を購読してください。CliffTech

私の記事をお読みいただき、ありがとうございます。お楽しみいただけたら幸いです。ご質問がある場合は、お気軽にお問い合わせください。

ソーシャルメディアで私とつながる: