TypeScriptでの非同期処理|async、awaitを徹底解説

※本サイトにはプロモーション・広告が含まれています。

(最終更新月:2023年11月)

✔当記事は以下のような疑問をお持ちの方にオススメです

「TypeScriptとasyncを併用する方法を知りたい」

「TypeScriptでの非同期処理の書き方について詳しく知りたい」

「実際にTypeScriptでasync/awaitを使った例を見てみたい」

✔当記事で皆さんに紹介する内容

  • TypeScriptでの非同期処理の基礎知識
  • async/awaitを用いたTypeScriptのコーディングのコツ
  • 実務で役立つasync/awaitの実践的なコード例

当記事では、単にTypeScriptの非同期処理について概説するだけでなく、async/awaitという強力な機能をどのように活用してコードを書くのかについて、現場で使える具体例を交えて幅広くご説明します。

最後まで読んでいただけると、TypeScriptでの非同期プログラミングに自信を持って挑めるでしょう。

ぜひ最後までご覧ください。

筆者プロフィール

筆者プロフィールアイコン

【現職】プロジェクトマネージャー/SE(元外資系金融機関営業)

【副業】ブログ(月間15万PV)/YouTube/Web・アプリ制作

コミュニケーション × ビジネススキル × 文章力 × プログラミングスキルを活かし、30後半からのIT系職へシフト。当サイトでは、実際に手を動かせるWebアプリの開発を通じて、プログラミングはもちろん、IT職に必要な情報を提供していきます。

【当ブログで紹介しているサイト】

当サイトチュートリアルで作成したデモ版日報アプリ

Django × Reactで開発したツール系Webアプリ

✔人に見せても恥ずかしくないコードを書こう

「リーダブルコード」は、わかりやすく良いコードの定義を教えてくれる本です。

  • 見るからにきれいなコードの書き方
  • コードの分割方法
  • 変数や関数の命名規則

エンジニアのスタンダートとすべき基準を一から解説しています。

何回も読むのに値する本なので、ぜひ手にとって読んでみてください。

非同期処理の基礎理解

ここでは、非同期処理について基礎的な理解を深めます。

非同期処理を理解することは、モダンなWeb開発において不可欠です。

  • 同期処理と非同期処理の比較
  • Promiseとは:基礎概念の解説
  • Promiseを活用する利点とシナリオ

同期処理と非同期処理の比較

同期・非同期処理は、それぞれ以下のように定義できます。

  • 同期処理: 命令が順番に実行される処理方法
  • 非同期処理: ひとつの処理が終わるのを待たずに次の命令が実行される方法

JavaScriptにおいてsetTimeout()を用いると、指定した時間が経過した後にコールバック関数が実行されますが、その間にほかのスクリプトが実行される余地が生まれます。

これが非同期処理の典型例です。

console.log('Start'); 
setTimeout(() => { console.log('Timer done!'); }, 2000);
console.log('End'); // このコードはすぐに実行されます

上記のコードは、「Start」→「End」→「Timer done!」の順番でログが出力されることを示しています。

Promiseとは:基礎概念の解説

Promiseは、非同期操作が終わった後の結果を表すオブジェクト。

非同期処理の成功(fulfill)または失敗(reject)を管理します。

新しいPromiseは以下のように作成されます。

let promise = new Promise<string>((resolve, reject) => { 
 // 非同期処理 
 if (/* 成功の条件 */) { resolve('成功の値'); } 
 // 失敗の処理
 else { reject('失敗の理由'); } });

このPromiseオブジェクトは、非同期処理の完了を待ち、処理が終わったらthenやcatchメソッドで結果を受け取ります。

Promiseを活用する利点とシナリオ

Promiseを活用する最大の利点は、非同期処理のコードが読みやすく、管理しやすくなること

とくに以下のように、結果がすぐには得られない処理を扱う際に有効です。

  • ネットワークからデータを取得する処理
  • 時間がかかるデータベースのクエリ

次のコードはネットワークリクエストをPromiseでおこなう簡単な例です。

function fetchData(url: string): Promise<any> { 
 return fetch(url) // fetchはネットワークリクエストを行うWeb API 
 .then(response => response.json()); 
} 

//実際に使ってみる
fetchData('https://api.example.com/data')
 .then(data => { console.log(data); // 取得したデータをコンソールに出力 })
 .catch(error => { console.error('Error fetching data:', error); });

fetchData関数は、指定されたURLからデータを非同期で取得し、JSON形式で解析して返すPromiseを返します。

TypeScriptのasync関数

async関数は、非同期処理をシンプルに記述するための便利な構文です。

async関数を使用することで、Promiseの処理をより直感的に扱えます。

  • async関数の定義と基本構文
  • asyncメソッドとクラスの関係
  • 非同期関数の戻り値の解説
  • 非同期エラー処理:rejectの使い方
  • async関数内での例外処理

async関数の定義と基本構文

async関数を記述する際のルールは以下の2点。

  • 内部で非同期処理をおこなう関数の前にasyncキーワードを付けることで定義
  • async関数内ではawaitキーワードを使用して、Promiseの完了を待ち受ける

基本的な構文は以下の通りです。

async function asyncFunction(): Promise<void> { 
 const result = await someAsyncOperation(); 
 console.log(result); // 結果を出力 
}

asyncFunctionは非同期処理someAsyncOperationを呼び出し、その結果を待ってからconsole.logで出力。

この関数自体もPromiseを返すため、呼び出し側ではthenメソッドを使って処理を続けられます。

asyncメソッドとクラスの関係

TypeScriptでは、クラスのメソッドとしてもasync関数を定義できます。

クラスのインスタンスのコンテキスト内で非同期処理をエレガントに扱うことが出来るようになるでしょう。

例えば、次のようにクラスメソッドをasync関数として定義します。

class DataFetcher { 
 async fetchData(url: string): Promise<any> { 
  try { 
    const response = await fetch(url); 
    const data = await response.json(); 
    return data; 
  } catch (error) { 
    throw new Error('Data fetch failed'); 
  }
 } 
} 
let fetcher = new DataFetcher();
fetcher.fetchData('https://api.example.com/data')
 .then(data => { console.log(data); })
 .catch(error => { console.error(error); });

DataFetcherクラスのfetchDataメソッドは、指定されたURLから非同期でデータを取得して返すものになります。

非同期関数の戻り値の解説

async関数は常にPromiseを返すというルールがあります。

これは、async関数内でreturnされた値が、実際にはPromise.resolve()によって解決されるPromiseとなるためです。

例えば、次の関数は明示的に値をPromiseで包む必要がなく、簡潔に書けます。

async function getNumber(): Promise<number> {
  return 42; // Promiseを返す必要がない 
} 

getNumber().then(value => { console.log(value); // 42が出力される });

async関数からのreturnは直ちにPromiseにラップされ、thenメソッドで結果を取り出せます。

非同期エラー処理:rejectの使い方

async関数内では、エラーが発生した場合にrejectするPromiseを返すには、例外をthrowすることによって可能です。

例外は、関数呼び出し側でcatchメソッドを使ってキャッチされるエラーとして扱われます。

以下はその使用例です。

async function mayFailOperation(): Promise<void> { 
 if (/* 失敗する条件 */) { throw new Error('Operation failed'); } 
 // 成功時の処理 
} 

mayFailOperation()
 .then(() => { console.log('Success!'); })
 .catch(error => { console.error('Error:', error); });

mayFailOperation関数は処理が失敗する条件によってErrorをthrowし、thenとcatchのメソッドで処理の成否を処理します。

async関数内での例外処理

JavaScriptやTypeScriptにおける例外処理にはtry…catch構文を使用しますが、async関数内でも同様です。

awaitキーワードを使用した非同期処理が例外を投げた場合、tryブロック内でそれをキャッチできます。

以下にその例を示します。

async function process(): Promise<void> { 
 try { 
  const result = await someAsyncFunction();
  console.log('Result received:', result); 
 } catch (error) { 
  console.error('Error during process:', error); 
 } 
}

process関数では、someAsyncFunctionが例外を投げた場合、catchブロックがそれをキャッチしてエラー処理をおこないます。

これにより非同期関数のエラーをより適切に管理できるのです。

awaitの活用法

awaitキーワードは、非同期処理の完了を待つことを可能にする重要な機能です。

正しい使い方をマスターすることで、コードの明瞭性を保ちつつ非同期処理を実装できます。

  • awaitキーワードの基本
  • シンプルなawait使用例
  • アクセス修飾子との併用
  • 並行処理とawaitの応用

awaitキーワードの基本

awaitキーワードは、Promiseがresolveまたはrejectされるまで関数の実行を一時停止します。

このキーワードはasync関数内でのみ使用可能であり、直後にPromiseを返す式が続きます。

使用例は以下の通りです。

async function getTodo(id: number): Promise<void> { 
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const todo = await response.json();
  console.log('Todo:', todo); 
}

getTodo関数は特定のIDに対応するToDoを取得し、console.logで出力します。

awaitは非同期のfetch処理が完了するまで待つ点に注意してください。

シンプルなawait使用例

awaitキーワードは複数の非同期処理が必要なシナリオでも直感的に使えます。

次のコードは、異なるリソースからデータを順番に取得する例です。

async function getUserAndPosts(userId: number): Promise<void> { 
  const userResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`); 
  const user = await userResponse.json(); 
  const postsResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`); 
  const posts = await postsResponse.json(); 
  console.log('User:', user, 'Posts:', posts); } 

getUserAndPosts関数は、ユーザー情報とそのユーザーの投稿情報を取得するために2回の非同期fetchを行い、結果をconsole.logで出力します。

アクセス修飾子との併用

TypeScriptでは、メソッドやプロパティにアクセス修飾子(例:public, private)を付けることで、クラス外からのアクセスを制御できます。

awaitキーワードとアクセス修飾子を併用することも可能です。

例として、次のようなプライベートメソッドを使った例があります。

class DataLoader { 
  private async fetchData(url: string): Promise<any> { 
    const response = await fetch(url); 
    const data = await response.json(); 
    return data; 
  }
  public async load() { 
    const data = await this.fetchData('https://api.example.com/data'); 
   console.log(data); 
  }
} 

const loader = new DataLoader(); 
loader.load();

DataLoaderクラスのfetchDataメソッドはプライベートであり、クラス内のメソッドからのみアクセスできます。

loadメソッドは公開されており、外部からデータのロードをトリガーできます。

並行処理とawaitの応用

複数の非同期処理を並列でおこなう場合、Promise.allメソッドで、全ての処理が完了するのを効率良く待てます

ここでも使われるのは、awaitです。

次の例では、異なるAPIエンドポイントから同時にデータを取得する方法を示します。

async function fetchMultipleUrls(urls: string[]): Promise<void> { 
  const promises = urls.map(url => fetch(url).then(res => res.json())); 
  const results = await Promise.all(promises); 
  console.log('Results:', results); 
}

fetchMultipleUrls関数は複数のURLに対してfetchを並行実行し、全てのPromiseが解決された時点での結果をconsole.logで出力します。

TypeScriptにおけるasync/awaitのパターン

TypeScriptには、async/awaitを使用する上でいくつかのパターンがあります。

これらを理解しておくことで、より効果的に非同期処理をコーディングすることが可能です。

  • async/awaitの基礎パターン
  • TypeScriptのジェネリックを使ったPromiseの展開
  • 非同期操作の組み合わせとロジック

async/awaitの基礎パターン

async/awaitの基本的な使用パターンは、非同期処理を同期処理のように扱える点にあります。

この基本パターンは非同期関数内で他の非同期関数を呼び出し、その処理完了を待ちつつ、コードは直線的に読めるようにすることです。

例として以下のようなコードが考えられます。

async function fetchAndProcessData(url: string): Promise<any> { 
  const data = await fetch(url).then(res => res.json()); 
  // データを加工する何らかの処理 
  return processedData; 
} 

fetchAndProcessData関数では、fetchを使ってデータを取得し、そのデータを加工した後に結果を返しています。

この関数は、awaitを使って非同期処理を待っている間も、複雑な処理がなければぱっと見で処理の流れが直感的です。

TypeScriptのジェネリックを使ったPromiseの展開

TypeScriptではジェネリックを活用し、Promiseが解決される値の型を明示的に指定できます。

これにより、コンパイル時の型チェックでの安全性が高まるのです。

次の例では、特定の型のデータを取得する非同期関数をジェネリックを使って定義しています。

async function fetchTypedData<T>(url: string): Promise<T> { 
  const response = await fetch(url); 
  const data: T = await response.json(); return data; 
}

fetchTypedData関数はジェネリック型Tを受け取り、この型のデータを非同期に取得して返します。

これにより呼び出し側では、取得されるデータの型を明確に指定して使用できます。

非同期操作の組み合わせとロジック

複数の非同期操作を組み合わせる場合、async/awaitを使って複雑なロジックをよりシンプルな記述が可能です。

例えば、一連のAPIリクエストを行い、それぞれの結果に基づいて次のリクエストを決定するような場合です。

async function complexOperation(): Promise<void> { 
  const result1 = await fetch('https://api.example.com/data1')
    .then(res => res.json()); 
  if (result1.needMoreData) { 
    const result2 = await fetch('https://api.example.com/data2')
      .then(res => res.json()); 
    // result1とresult2のデータを組み合わせて処理 
  } 
  // 最終的な結果を処理 
} 

complexOperation関数では、最初のAPIリクエストの結果に基づいて次のリクエストをおこない、それらのデータを組み合わせて処理をおこないます。

このようにasync/awaitを利用することで、条件に応じた複数の非同期処理の連携が直感的なコードで書けるでしょう。

実用例:TypeScriptによる非同期処理

TypeScriptを用いた非同期処理の具体的な実用例を見ていきましょう。

実際の開発シナリオで非同期処理をどのように扱えば良いのか、コード例を通じて学びます。

  • APIリクエストの非同期処理の実例
  • ファイル操作における非同期処理の応用
  • データベース操作とasync/await
  • ユーザーインタフェースとの非同期通信

APIリクエストの非同期処理の実例

Web APIへのリクエストは、非同期処理の実用的な例です。

以下に、外部APIからユーザー情報を取得し、そのデータを処理するasync関数を示します。

interface User { 
  id: number; name: string; 
  email: string; 
  // 他のプロパティ 
} 

async function getUser(id: number): Promise<User> { 
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`); 
  const user: User = await response.json(); 
  return user; 
}

getUser関数はユーザーIDを受け取り、非同期にユーザー情報を取得してそのオブジェクトを返すもの。

この関数はAPIのレスポンスがUserインターフェイスの形をしていることを想定しています。

ファイル操作における非同期処理の応用

Node.jsを利用したファイル操作は、非同期処理が非常に活躍する例です。

以下に、TypeScriptでファイルを非同期に読み込む関数を示します。

import { promises as fs } from 'fs'; 

async function readFile(filePath: string): Promise<string> { 
  const data = await fs.readFile(filePath, { encoding: 'utf-8' }); 
  return data; 
}

readFile関数はファイルパスを受け取り、そのファイルを非同期に読み込んでその内容を文字列として返すもの。

ここではfsモジュールのpromises APIを使用して非同期にファイル読み取りをおこないます。

データベース操作とasync/await

データベースへのクエリ実行も、非同期処理において一般的なユースケースです。

以下に、データベースから特定のユーザー情報を取得する例を示します。

// 仮のdatabase module 
import database from './database'; 

async function getUserFromDatabase(userId: number): Promise<User> { 
  const user: User = await database.query('SELECT * FROM users WHERE id = ?', [userId]); 
  return user; 
}

getUserFromDatabase関数は、ユーザーIDを受け取り、非同期にデータベースをクエリしてユーザー情報を取得して返します。

database.queryはPromiseを返す仮想のメソッドです。

ユーザーインタフェースとの非同期通信

ユーザーインタフェース(UI)との非同期通信も非常に重要です。

例えば、ユーザーがフォームを送信した際に、その情報を非同期にサーバーへ送信し、結果に応じてUIを更新する処理が考えられます。

async function submitForm(formData: FormData): Promise<void> { 
  const response = await fetch('/submit', { method: 'POST', body: formData }); 
  const result = await response.json(); 
  // 結果に応じてUIを更新する処理 
}

submitForm関数はフォームデータを受け取り、非同期にサーバーへPOSTリクエストを送信。

その後、応答を受け取り、JSONとして解析してUIの更新処理をおこないます。

エラーハンドリングとデバッグ

非同期処理では、とくにエラーハンドリングとデバッグが重要です。

不測のエラーや予期せぬ挙動を避け、信頼性高いアプリケーションを作成するための方法を学んでいきましょう。

  • トライアンドキャッチによるエラー処理
  • TypeScriptでの非同期デバッグテクニック
  • Promiseチェーンとエラー伝播

トライアンドキャッチによるエラー処理

async関数内での例外処理には、try…catch構文が有効です。

非同期関数が例外を投げるとそのエラーを捕捉し、適切なエラーハンドリングをおこなえます。

async function safeFetch(url: string): Promise<any> { 
  try { 
    const response = await fetch(url); 
    return await response.json(); 
  } catch (error) { console.error('Fetch error:', error); 
    // 必要に応じたエラーハンドリング 
  } 
} 

safeFetch関数はtryブロック内で非同期関数を実行し、例外が発生した場合、catchブロック内でエラーをキャッチします。

ユーザーにフィードバックを与えたり、プログラムの安定性を保つための処理をおこなえます。

TypeScriptでの非同期デバッグテクニック

TypeScriptでの非同期処理のデバッグには、適切なツールとテクニックを用いることが効果的です。

とくに、ソースマップを利用することで、コンパイル後のJavaScriptではなく、元のTypeScriptのソースコードでブレークポイントを設定したり、ステップ実行したりが可能です。

{ 
  "compilerOptions": { 
    "sourceMap": true, // ソースマップを有効にする 
  // その他のコンパイラオプション 
  } 
}

上記のtsconfig.json設定は、TypeScriptのコンパイルオプションでソースマップを有効にすることを示しています。

これにより、TypeScriptのコードをデバッグする際も実際のソースコードで追跡が可能です。

Promiseチェーンとエラー伝播

Promiseを用いた非同期処理では、エラーが発生するとそれが伝播し、最終的にはcatchメソッドでキャッチされます。

しかし、Promiseチェーン内での適切なエラーハンドリングが省かれると、エラーが無視されたり、扱いにくくなったりする原因となります。

function processData(data: any): any { 
  if (/* データに問題がある場合 */) { 
    throw new Error('Invalid data'); 
  } 
  // データ処理 
  return processedData; 
} 

fetchData().then(processData)
 .then(result => { console.log('Processed data:', result); }) 
 .catch(error => { console.error('An error occurred:', error); });

fetchData関数が成功してデータを取得し、processData関数でそのデータを処理するPromiseチェーンの例です。

processData関数でエラーが投げられると、チェーンの最後のcatchメソッドでそれをキャッチし、適切に処理します。

FAQとベストプラクティス

非同期処理に関するよくある質問(FAQ)と、より良い非同期プログラミングのためのベストプラクティスを紹介します。

型安全で保守しやすいコードを書くために重要な情報をまとめてみましょう。

  • async/awaitに関するよくある質問
  • 非同期プログラミングのベストプラクティスの紹介
  • 大規模プロジェクトでのasync/awaitの管理

async/awaitに関するよくある質問

以下の質問を事前に確認しておきましょう。

Q: async関数から普通の値を返した場合、どのように扱われますか?

A: async関数から返される値は、Promise.resolve()を使って自動的にPromiseにラップされます。

出力されるのは、そのままの値ではなく、その値を解決するPromiseです。

Q: awaitを使わずにasync関数を呼び出した場合、どうなりますか?

A: awaitを使わずにasync関数を呼び出すと、その関数から返されるPromiseが即座に取得されますが、そのPromiseが解決または拒否されるのを待つことはありません。

awaitを使うと、非同期関数が結果を返すまで実行を一時停止します。

非同期プログラミングのベストプラクティスの紹介

非同期プログラミングのベストプラクティスには、複数の要点があります。

  • いつでもエラーハンドリングをおこなう
  • 変数を適切にスコープ内に閉じ込める
  • 不要なawaitの使用を避ける

また、コードの可読性を保ちつつ、同時に複数の非同期処理を管理する方法(例:Promise.all())を活用することも大切です。

【便利なヒント】

大規模プロジェクトでは、async/awaitの使用がコードベース全体で一貫していることが望ましいです。

  • 非同期処理のチェーンでは、.then()や.catch()の代わりに、async/awaitを使うとより視覚的にわかりやすくなる
  • 大量のデータを扱う場合や、高速なレスポンスが必要な時は、並行処理を意識して設計する
  • async/awaitを用いながらも、適切な抽象化とモジュール化を行い、関数の責務を明確にする

大規模プロジェクトでのasync/awaitの管理

一貫性を保つためには、プロジェクト内で非同期処理を行う場合のガイドラインを設け、それに従うことが重要です。

また、型システムの恩恵を受けるためにも、非同期関数の戻り値の型を明確にし、可能な限り型安全なコードを書きましょう。

コードレビューや自動テストを活用して非同期処理が正しく行われているかどうかを確認することも推奨されます。

まとめ

当記事では、TypeScriptにおけるasync/awaitを用いた非同期処理について学習してきました。

非同期処理は現代のWeb開発に不可欠であり、これをマスターすることは任意のアプリケーションやサービスを開発する上で重要なスキルとなります。

当記事が、TypeScriptを用いたプログラミングの理解を深め、より良い非同期処理を書く手助けになれば幸いです。

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