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);
}
}