TypeScript非同期パターン完全ガイド2025:最新のベストプラクティスと実装テクニック

TypeScript非同期パターン完全ガイド2025:最新のベストプラクティスと実装テクニック
TypeScriptにおける非同期処理の基礎と進化
TypeScriptでの非同期処理は、モダンなウェブアプリケーション開発において不可欠なスキルです。2025年現在、非同期処理のパターンは進化を続け、より効率的で保守性の高いコードを書くための新しいアプローチが登場しています。
非同期処理の歴史的変遷
JavaScriptとTypeScriptにおける非同期処理の歴史を振り返ると、その進化は明らかです:
- コールバック関数: 初期の非同期処理方法で、「コールバック地獄」と呼ばれる問題を引き起こしました
- Promise: ES2015で導入され、非同期処理の連鎖を読みやすく書けるようになりました
- async/await: ES2017で導入され、非同期コードを同期的に書けるようになりました
- 現代のパターン: 2025年では、これらの基本技術を組み合わせた高度なパターンが一般的になっています
// コールバック方式(古い方法)
function fetchData(callback: (data: any, error?: Error) => void): void {
setTimeout(() => {
try {
const data = { name: "TypeScript", version: "5.4" };
callback(data);
} catch (error) {
callback(null, error as Error);
}
}, 1000);
}
// Promise方式
function fetchDataPromise(): Promise<{ name: string; version: string }> {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
const data = { name: "TypeScript", version: "5.4" };
resolve(data);
} catch (error) {
reject(error);
}
}, 1000);
});
}
// async/await方式(現代的)
async function fetchDataModern(): Promise<{ name: string; version: string }> {
// 実際のアプリケーションではここでAPIリクエストなどを行う
return { name: "TypeScript", version: "5.4" };
}
TypeScriptは型システムを通じて、これらの非同期パターンに安全性を提供します。特に、Promiseの戻り値の型を明示することで、非同期処理の結果の型を保証できます。
2025年のPromiseパターン:高度な使い方
Promiseは非同期処理の基盤として今も重要な役割を果たしています。2025年においては、より洗練された使い方が標準となっています。
Promise Combinatorsの効果的な活用
TypeScriptでは、複数のPromiseを組み合わせるための様々なメソッドが提供されています:
// 複数のAPIエンドポイントからデータを取得する例
async function fetchDashboardData(userId: string): Promise<DashboardData> {
// 並列実行:すべてのPromiseが完了するまで待機
const [user, settings, notifications] = await Promise.all([
fetchUser(userId),
fetchUserSettings(userId),
fetchUserNotifications(userId)
]);
return {
user,
settings,
notifications,
lastUpdated: new Date()
};
}
// 最初に完了したPromiseの結果を使用
async function fetchFromFastestMirror<T>(urls: string[]): Promise<T> {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return Promise.any(promises); // 最初に成功したPromiseの結果を返す
}
// すべてのPromiseの結果を取得(失敗したものも含む)
async function attemptAllOperations<T>(operations: Promise<T>[]): Promise<(T | Error)[]> {
const results = await Promise.allSettled(operations);
return results.map(result => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return result.reason;
}
});
}
Promiseチェーンの最適化
長いPromiseチェーンは読みにくくなりがちですが、適切に構造化することで可読性を向上させることができます:
// 構造化されたPromiseチェーンの例
function processUserData(userId: string): Promise<ProcessedUserData> {
return fetchUser(userId)
.then(user => {
// 中間結果をログに記録
console.log(`User ${user.name} fetched`);
// 次の処理に必要なデータだけを渡す
return {
userId: user.id,
preferences: user.preferences
};
})
.then(({ userId, preferences }) => {
// 並列でデータを取得
return Promise.all([
fetchRecommendations(userId, preferences),
fetchHistory(userId)
]);
})
.then(([recommendations, history]) => {
// 最終的な処理結果を返す
return {
recommendations: recommendations.filter(r => r.score > 0.8),
recentItems: history.items.slice(0, 5)
};
})
.catch(error => {
// エラーハンドリングと回復ロジック
console.error(`Failed to process user data: ${error.message}`);
return {
recommendations: [],
recentItems: []
};
});
}
Promiseのキャンセル処理
2025年のTypeScriptでは、AbortControllerを使用したPromiseのキャンセル処理が標準的になっています:
// キャンセル可能なAPI呼び出し
function fetchWithTimeout<T>(url: string, timeoutMs = 5000): Promise<T> {
const controller = new AbortController();
const { signal } = controller;
// タイムアウト用のタイマー
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
return fetch(url, { signal })
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json() as Promise<T>;
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
});
}
async/awaitの最適化パターン
async/await構文は非同期コードを同期的に書けるようにする強力な機能ですが、効果的に使用するにはいくつかのパターンを理解する必要があります。
エラーハンドリングの最適化
try/catchブロックを使用したエラーハンドリングは、async/await構文の大きな利点の一つです:
// 構造化されたエラーハンドリング
async function fetchUserProfile(userId: string): Promise<UserProfile | null> {
try {
const user = await fetchUser(userId);
try {
// ネストされたtry/catchで特定の操作のエラーを個別に処理
const posts = await fetchUserPosts(user.id);
return {
...user,
recentPosts: posts.slice(0, 5)
};
} catch (postError) {
// 投稿の取得に失敗しても、ユーザー情報は返す
console.warn(`Failed to fetch posts: ${postError.message}`);
return {
...user,
recentPosts: []
};
}
} catch (userError) {
// ユーザー情報の取得に失敗した場合
console.error(`Failed to fetch user: ${userError.message}`);
return null;
}
}
並列処理の最適化
async/await構文を使用する際、複数の非同期処理を効率的に並列実行することが重要です:
// 非効率な逐次実行
async function fetchDataSequential(ids: string[]): Promise<Data[]> {
const results = [];
// 一つずつ順番に実行(遅い)
for (const id of ids) {
const data = await fetchData(id);
results.push(data);
}
return results;
}
// 効率的な並列実行
async function fetchDataParallel(ids: string[]): Promise<Data[]> {
// すべてのPromiseを同時に開始し、結果を待つ(速い)
const promises = ids.map(id => fetchData(id));
return Promise.all(promises);
}
// 制御された並列実行(バッチ処理)
async function fetchDataInBatches(ids: string[], batchSize = 5): Promise<Data[]> {
const results: Data[] = [];
// IDsをバッチに分割
for (let i = 0; i < ids.length; i += batchSize) {
const batchIds = ids.slice(i, i + batchSize);
// バッチ内のリクエストを並列実行
const batchPromises = batchIds.map(id => fetchData(id));
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return results;
}
async/awaitとジェネレーターの組み合わせ
複雑な非同期フローを制御するために、async/awaitとジェネレーターを組み合わせることができます:
// ジェネレーターを使った段階的なデータ処理
async function* processLargeDataset<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
batchSize = 100
): AsyncGenerator<R[], void, unknown> {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
// バッチを並列処理
const promises = batch.map(item => processor(item));
const results = await Promise.all(promises);
// 各バッチの結果を生成
yield results;
// 進捗報告
console.log(`Processed ${Math.min(i + batchSize, items.length)}/${items.length} items`);
}
}
// 使用例
async function runDataProcessing() {
const items = await fetchLargeDataset();
const processor = processLargeDataset(items, processItem, 50);
let processedCount = 0;
// 各バッチの結果を処理
for await (const batchResults of processor) {
await saveBatchResults(batchResults);
processedCount += batchResults.length;
// 進捗を更新
updateProgressUI(processedCount, items.length);
}
console.log('Processing complete!');
}
高度なエラーハンドリングパターン
非同期処理におけるエラーハンドリングは、堅牢なアプリケーションを構築する上で非常に重要です。2025年のTypeScriptでは、より洗練されたエラーハンドリングパターンが一般的になっています。
型安全なエラーハンドリング
TypeScriptの型システムを活用して、より安全なエラーハンドリングを実現できます:
// カスタムエラークラスの定義
class APIError extends Error {
constructor(
message: string,
public statusCode: number,
public errorCode: string
) {
super(message);
this.name = 'APIError';
// Errorオブジェクトのプロトタイプチェーンを正しく設定
Object.setPrototypeOf(this, APIError.prototype);
}
isClientError(): boolean {
return this.statusCode >= 400 && this.statusCode < 500;
}
isServerError(): boolean {
return this.statusCode >= 500;
}
// エラーに応じた適切なユーザーメッセージを返す
getUserMessage(): string {
if (this.statusCode === 401) {
return 'セッションが切れました。再度ログインしてください。';
} else if (this.statusCode === 403) {
return 'この操作を行う権限がありません。';
} else if (this.statusCode === 404) {
return '要求されたリソースが見つかりませんでした。';
} else if (this.isServerError()) {
return 'サーバーエラーが発生しました。しばらく経ってからもう一度お試しください。';
} else {
return 'エラーが発生しました。';
}
}
}
// Result型パターンの実装
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
// Result型を返す関数
async function fetchUserSafely(userId: string): Promise<Result<User, APIError>> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
success: false,
error: new APIError(
errorData.message || 'Failed to fetch user',
response.status,
errorData.code || 'UNKNOWN_ERROR'
)
};
}
const user = await response.json();
return { success: true, value: user };
} catch (error) {
return {
success: false,
error: new APIError(
error instanceof Error ? error.message : 'Unknown error',
500,
'NETWORK_ERROR'
)
};
}
}
// 使用例
async function displayUserProfile(userId: string): Promise<void> {
const result = await fetchUserSafely(userId);
if (result.success) {
// 型安全:TypeScriptはresult.valueがUser型であることを認識
renderUserProfile(result.value);
} else {
// 型安全:TypeScriptはresult.errorがAPIError型であることを認識
showErrorMessage(result.error.getUserMessage());
// エラーの種類に応じた処理
if (result.error.statusCode === 401) {
redirectToLogin();
} else if (result.error.isServerError()) {
reportErrorToMonitoring(result.error);
}
}
}
リトライメカニズム
一時的なエラーに対処するためのリトライメカニズムも重要です:
// 指数バックオフを使用したリトライ関数
async function withRetry<T>(
operation: () => Promise<T>,
options: {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
backoffFactor?: number;
retryableErrors?: (error: unknown) => boolean;
} = {}
): Promise<T> {
const {
maxRetries = 3,
initialDelay = 300,
maxDelay = 10000,
backoffFactor = 2,
retryableErrors = (error) => true
} = options;
let lastError: unknown;
let delay = initialDelay;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
// 最後の試行でエラーが発生した場合、またはリトライ可能なエラーでない場合はエラーをスロー
if (attempt === maxRetries || !retryableErrors(error)) {
throw error;
}
console.log(`Attempt ${attempt + 1}/${maxRetries} failed. Retrying in ${delay}ms...`);
// 指定された時間だけ待機
await new Promise(resolve => setTimeout(resolve, delay));
// 次回の待機時間を計算(指数バックオフ)
delay = Math.min(delay * backoffFactor, maxDelay);
}
}
// コンパイラのためのフォールバック(実際には到達しない)
throw lastError;
}
まとめ
2025年のTypeScriptにおける非同期パターンは、単なる基本的な非同期処理から、より洗練された高度なパターンへと進化しています。この記事で紹介した主なポイントは以下の通りです:
- Promiseの高度な使い方:Promise Combinatorsの活用、チェーンの最適化、キャンセル処理
- async/awaitの最適化:効率的なエラーハンドリング、並列処理の最適化、ジェネレーターとの組み合わせ
- 高度なエラーハンドリング:型安全なエラーハンドリング、Result型パターン、リトライメカニズム
これらのパターンを適切に組み合わせることで、より保守性が高く、パフォーマンスの良い非同期コードを書くことができます。TypeScriptの型システムを最大限に活用することで、コンパイル時にエラーを検出し、より堅牢なアプリケーションを構築することができるでしょう。
非同期処理は常に進化しており、新しいパターンやベストプラクティスが登場し続けています。最新の動向に注目しながら、自分のプロジェクトに最適なパターンを選択していくことが重要です。
このトピックはこちらの書籍で勉強するのがおすすめ!
この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!
おすすめコンテンツ
おすすめIT技術2025/5/1TypeScript開発を劇的に効率化する13のベストプラクティス
TypeScriptプロジェクトの開発効率を高めるベストプラクティスを紹介します。プロジェクト設定から型活用テクニック、コードの最適化まで、実務で即役立つ具体例とともに解説し、TypeScriptの真...
続きを読む TypeScript2025/5/16TypeScript非同期処理パターン完全ガイド:エラーハンドリングから並行処理まで
TypeScriptにおける非同期処理の基本から応用までを網羅。Promiseの使い方、async/awaitのベストプラクティス、エラーハンドリング、並行処理パターンまで実践的なコード例とともに解説...
続きを読む クラウドネイティブ2025/5/15クラウドネイティブアプリケーション開発完全ガイド:2025年最新のベストプラクティス
モダンなクラウドネイティブアプリケーション開発の全てを解説。マイクロサービス、コンテナ、CI/CD、オブザーバビリティの実装方法と、スケーラブルで堅牢なアプリケーションを構築するためのベストプラクティ...
続きを読む CSS2025/6/2CSS Container Queriesを使った次世代レスポンシブデザイン完全ガイド:2025年最新の実装テクニック
CSS Container Queriesの基本から実践まで完全解説。メディアクエリの限界を超えた次世代レスポンシブデザインの実装方法とコード例を詳しく紹介します。
続きを読む JavaScript2025/6/1JavaScript ES2024新機能完全ガイド!モダン開発で差をつける最新構文と実装例
JavaScript ES2024の新機能を実践的なコード例とともに詳しく解説します。Array.prototype.toSorted、Object.groupBy、Promise.withResol...
続きを読む GraphQL2025/5/2【2025年最新】GraphQLを使った高速APIの設計パターンと実装テクニック
GraphQLを活用した効率的なAPI設計のベストプラクティスとコード例を紹介。スキーマ設計からパフォーマンス最適化まで、実務で使える具体的な実装テクニックとN+1問題の解決策を解説します。
続きを読む 生成AI2025/5/12生成AI開発者のためのエラーハンドリング完全ガイド:問題解決のベストプラクティス
生成AIアプリケーション開発で直面する一般的なエラーとその解決法を解説。プロンプトエンジニアリングからAPI連携、メモリ管理まで、実践的なコード例とトラブルシューティング手法を紹介します。これ一冊で生...
続きを読む React2025/5/1Next.jsとTypeScriptで作る高速SSGブログ:初心者でも簡単に実装できる完全ガイド
Next.jsとTypeScriptを組み合わせたSSGブログの作り方を解説します。静的サイト生成の利点から実装方法、デプロイまで、初心者でも理解できるようステップバイステップで説明。コードサンプル付...
続きを読む