TypeScript 速習チュートリアル

TypeScriptによる非同期プログラミングを

TypeScriptは、JavaScriptの非同期処理機能に対して静的な型付けを提供し、非同期コードの予測可能性と保守性を飛躍的に高めます。
本ガイドでは、基本的な async/await から高度なデザインパターンまでを詳しく解説します。

1. TypeScript における Promises

TypeScriptは、ジェネリクス(Generics)を使用してJavaScriptのPromiseを強化します。
Promise<T> は、型 T の値で完了するか、型 any の理由で失敗する非同期操作を表します。

重要なポイント:

  • Promise<T> - 解決された値の型が T であるジェネリック型
  • Promise<void> - 値を返さないPromise用
  • Promise<never> - 決して解決(Resolve)しないPromise用(稀なケース)

1.1 基本的な Promise の例

コード例

// 文字列を解決する型定義された Promise を作成
const fetchGreeting = (): Promise<string> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = Math.random() > 0.5;
      if (success) {
        resolve("こんにちは、TypeScript!");
      } else {
        reject(new Error("挨拶の取得に失敗しました"));
      }
    }, 1000);
  });
};

// 適切な型推論を伴う Promise の使用
fetchGreeting()
  .then((greeting) => {
    // TypeScript は 'greeting' が string であることを認識しています
    console.log(greeting.toUpperCase());
  })
  .catch((error: Error) => {
    console.error("エラー:", error.message);
  });

1.2 Promise の状態と型の流れ

Promise の状態フロー:

  • pending → fulfilled (値: T) // 成功ケース
  • pending → rejected (理由: any) // エラーケース

TypeScriptは型システムを通じてこれらの状態を追跡し、成功とエラーの両方のケースを適切に処理できるようにします。Promise<T> の型パラメータは、Promiseが解決されたときに期待される型をコンパイラに伝え、強力な型チェックとIDEのサポートを可能にします。

2. TypeScript での Async/Await

TypeScriptの async/await 構文は、Promiseを扱うためのよりクリーンな方法を提供します。非同期コードを同期コードのように記述でき、型安全性も維持されます。

2.1 Async/Await の主な利点

  • 可読性: 順次処理されるコードにより、ロジックを追いやすい。
  • エラーハンドリング: 同期・非同期両方のエラーに対して try/catch が使用可能。
  • デバッグ: 同期的なスタックトレースにより、デバッグが容易。
  • 型安全性: TypeScriptによる完全な型推論とチェック。

2.2 基本的な Async/Await の例

コード例

// APIレスポンス用の型定義
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

// User配列のPromiseを返す関数
async function fetchUsers(): Promise<User[]> {
  console.log('ユーザーを取得中...');
  // APIコールをシミュレート
  await new Promise(resolve => setTimeout(resolve, 1000));
  return [
    { id: 1, name: 'アリス', email: '[email protected]', role: 'admin' },
    { id: 2, name: 'ボブ', email: '[email protected]', role: 'user' }
  ];
}

// ユーザーを処理する非同期関数
async function processUsers() {
  try {
    // TypeScript は users が User[] 型であることを認識します
    const users = await fetchUsers();
    console.log(`${users.length} 名のユーザーを取得しました`);

    // 型安全なプロパティアクセス
    const adminEmails = users
      .filter(user => user.role === 'admin')
      .map(user => user.email);

    console.log('管理者メールリスト:', adminEmails);
    return users;
  } catch (error) {
    if (error instanceof Error) {
      console.error('ユーザー処理に失敗しました:', error.message);
    } else {
      console.error('不明なエラーが発生しました');
    }
    throw error; // 呼び出し元が処理できるように再スロー
  }
}

// 非同期関数の実行
processUsers()
  .then(users => console.log('処理完了'))
  .catch(err => console.error('処理失敗:', err));

2.3 非同期関数の戻り値の型

TypeScriptのすべての async 関数は、暗黙的にPromiseを返します。
戻り値の型は自動的にPromiseでラップされます。

async function getString(): string { } // エラー: Promise<string> を返す必要があります
async function getString(): Promise<string> { } // 正解

3. Promise.all による並列実行

複数の非同期操作を並列に実行し、すべてが完了するのを待ちます。

コード例

interface Product {
  id: number;
  name: string;
  price: number;
}

async function fetchProduct(id: number): Promise<Product> {
  console.log(`商品 ID: ${id} を取得中...`);
  await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
  return { id, name: `商品 ${id}`, price: Math.floor(Math.random() * 100) };
}

async function fetchMultipleProducts() {
  try {
    // すべての取得を並列に開始
    const [product1, product2, product3] = await Promise.all([
      fetchProduct(1),
      fetchProduct(2),
      fetchProduct(3)
    ]);

    const total = [product1, product2, product3]
      .reduce((sum, product) => sum + product.price, 0);
    console.log(`合計金額: $${total.toFixed(2)}`);
  } catch (error) {
    console.error('商品の取得中にエラーが発生しました:', error);
  }
}

fetchMultipleProducts();

4. 非同期操作のコールバックの型付け

従来のコールバックベースの非同期コードに対しても、TypeScriptはコールバックパラメータの適切な型付けを保証します。

コード例

// コールバック用の型定義
type FetchCallback = (error: Error | null, data?: string) => void;

// 型定義されたコールバックを受け取る関数
function fetchDataWithCallback(url: string, callback: FetchCallback): void {
  // 非同期操作をシミュレート
  setTimeout(() => {
    try {
      // 成功レスポンスをシミュレート
      callback(null, "レスポンスデータ");
    } catch (error) {
      callback(error instanceof Error ? error : new Error('不明なエラー'));
    }
  }, 1000);
}

// コールバック関数の使用
fetchDataWithCallback('https://api.example.com', (error, data) => {
  if (error) {
    console.error('エラー:', error.message);
    return;
  }
  
  // TypeScript は data が string (または undefined) であることを認識します
  if (data) {
    console.log(data.toUpperCase());
  }
});

5. Promise の組み合わせメソッド

TypeScriptは、複数のPromiseを処理するための強力なユーティリティメソッドを提供します。

  • Promise.all() - すべてのPromiseが解決するのを待つ(一つでも失敗すれば失敗)
  • Promise.race() - 最も早く完了(成功または失敗)したPromiseを返す
  • Promise.allSettled() - すべてが完了(成功または失敗)するのを待つ
  • Promise.any() - 最初に成功したPromiseを返す

5.1 Promise.race - タイムアウト処理への応用

APIリクエストのタイムアウトの実装などに役立ちます。

コード例

// タイムアウト用のヘルパー関数
const timeout = (ms: number): Promise<never> =>
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`${ms}ms でタイムアウトしました`)), ms)
  );

// タイムアウト付きのフェッチ関数
async function fetchWithTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number = 5000
): Promise<T> {
  return Promise.race([
    promise,
    timeout(timeoutMs).then(() => {
      throw new Error(`${timeoutMs}ms 後にリクエストがタイムアウトしました`);
    }),
  ]);
}

5.2 Promise.allSettled - すべての結果をハンドル

各操作の成功・失敗に関わらず、すべての完了を待つ場合に適しています。

コード例

const fetchData = async (id: number) => {
  if (Math.random() > 0.7) {
    throw new Error(`ID ${id} の取得に失敗しました`);
  }
  return { id, data: `Data for ${id}` };
};

async function processBatch(ids: number[]) {
  const promises = ids.map(id => fetchData(id));
  const results = await Promise.allSettled(promises);

  const successful = results
    .filter((result): result is PromiseFulfilledResult<{id: number, data: string}> => 
      result.status === 'fulfilled'
    )
    .map(r => r.value);

  const failed = results
    .filter((result): result is PromiseRejectedResult => 
      result.status === 'rejected'
    );

  console.log(`成功件数: ${successful.length}, 失敗件数: ${failed.length}`);
}

6. 非同期コードにおけるエラーハンドリング

6.1 カスタムエラークラス

ドメイン固有のエラータイプを作成し、より詳細なエラーハンドリングを実現します。

コード例

class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly details?: unknown
  ) {
    super(message);
    this.name = this.constructor.name;
  }
}

class NetworkError extends AppError {
  constructor(message: string, details?: unknown) {
    super(message, 'NETWORK_ERROR', details);
  }
}

async function fetchUserData(userId: string) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      if (response.status === 404) throw new Error('Not Found');
      throw new NetworkError('Server Error', { status: response.status });
    }
    return await response.json();
  } catch (error) {
    if (error instanceof AppError) throw error;
    throw new AppError('予期せぬエラーが発生しました', 'UNEXPECTED_ERROR', { cause: error });
  }
}

7. TypeScript での非同期イテレーション

TypeScriptは、型付けされた非同期イテレータと非同期ジェネレータをサポートしています。

コード例

// 非同期ジェネレータ関数
async function* generateNumbers(): AsyncGenerator<number, void, unknown> {
  let i = 0;
  while (i < 5) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i++;
  }
}

// 非同期ジェネレータの使用
async function consumeNumbers() {
  for await (const num of generateNumbers()) {
    // num が number 型であることを TypeScript は認識しています
    console.log(num * 2);
  }
}