TypeScript 非同期処理の型安全な書き方:Promise・async/await の型定義

TypeScript を使った非同期処理の開発で、「Cannot read property 'then' of undefined」「Argument of type 'Promise' is not assignable to parameter of type 'User'」といったエラーメッセージに悩まされていませんか?
非同期処理は現代の Web アプリケーション開発において避けて通れない要素ですが、TypeScript における適切な型定義を理解していないと、開発効率の大幅な低下や実行時エラーの原因となってしまいます。実際、多くの開発チームで非同期処理関連の型エラーが開発ボトルネックの主要因となっているのが現状です。
本記事では、Promise・async/await の型定義を完全にマスターし、型安全で保守性の高い非同期処理を実装するための実践的なテクニックを、豊富なコード例とともに解説します。読み終える頃には、非同期処理の型エラーに悩まされることなく、自信を持って開発を進められるようになるでしょう。
非同期処理の型エラーが開発現場に与える深刻な影響
非同期処理における型エラーは、単なる警告以上の深刻な問題を開発現場にもたらします。実際のプロジェクトではどのような影響があるのでしょうか。
開発効率への直接的ダメージ
非同期処理の型定義が曖昧だと、開発者は以下のような問題に直面します。
typescript// 型エラーが頻発する問題のあるコード例
async function fetchUserData(userId: string) {
// 戻り値の型が不明確
const response = await fetch(`/api/users/${userId}`);
return response.json(); // any型になってしまう
}
async function processUser() {
const userData = await fetchUserData('123'); // any型
// プロパティアクセスでエラーが発生する可能性
console.log(userData.name.toUpperCase()); // 実行時エラーのリスク
console.log(userData.email.split('@')); // 型チェックが効かない
}
このようなコードでは、IDE での自動補完も効かず、実行時エラーのリスクが高まります。
プロジェクト規模での影響分析
# | 影響項目 | 小規模プロジェクト | 中規模プロジェクト | 大規模プロジェクト |
---|---|---|---|---|
1 | デバッグ時間の増加 | +20% | +40% | +60% |
2 | コードレビュー時間の延長 | +15% | +30% | +50% |
3 | 新メンバーの学習コスト | 低 | 中 | 高 |
4 | リファクタリングの困難度 | 低 | 高 | 非常に高 |
5 | 実行時エラーの発生頻度 | 週 1 回 | 週 3 回 | 毎日 |
これらの問題を根本的に解決するためには、TypeScript の非同期処理における型システムを正しく理解し、適切に活用することが不可欠です。
非同期処理でよく発生する型エラー Top5 とその解決法
実際の開発現場で最も頻繁に遭遇する非同期処理の型エラーと、それぞれの確実な解決方法を詳しく解説します。
エラー 1:Promise 型の戻り値が正しく型推論されない
最も多い問題は、非同期関数の戻り値型がany
になってしまうケースです。
typescript// 問題のあるコード
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
return response.json(); // any型になる
}
// 解決策1:明示的な型注釈
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json() as User;
}
// 解決策2:型ガード付きの安全な実装
function isUser(data: any): data is User {
return (
typeof data === 'object' &&
typeof data.id === 'string' &&
typeof data.name === 'string' &&
typeof data.email === 'string'
);
}
async function fetchUserSafe(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
if (!isUser(data)) {
throw new Error('Invalid user data received');
}
return data;
}
エラー 2:Promise.all()の型推論の失敗
複数の非同期処理を並行実行する際に型が正しく推論されないケースです。
typescript// 問題のあるコード
async function fetchUserData(userId: string) {
const [user, posts, comments] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchComments(userId),
]); // 型が正しく推論されない
}
// 解決策:型注釈付きのPromise.all
interface Post {
id: string;
title: string;
content: string;
userId: string;
}
interface Comment {
id: string;
content: string;
userId: string;
postId: string;
}
async function fetchUserData(userId: string): Promise<{
user: User;
posts: Post[];
comments: Comment[];
}> {
const [user, posts, comments] = await Promise.all([
fetchUser(userId) as Promise<User>,
fetchPosts(userId) as Promise<Post[]>,
fetchComments(userId) as Promise<Comment[]>,
]);
return { user, posts, comments };
}
// さらに型安全なアプローチ
async function fetchUserDataTypeSafe(userId: string) {
const userPromise: Promise<User> = fetchUser(userId);
const postsPromise: Promise<Post[]> = fetchPosts(userId);
const commentsPromise: Promise<Comment[]> =
fetchComments(userId);
const [user, posts, comments] = await Promise.all([
userPromise,
postsPromise,
commentsPromise,
]);
return { user, posts, comments };
}
エラー 3:エラーハンドリングでの型の不整合
try-catch 文でのエラー処理において、エラー型が適切に処理されないケースです。
typescript// 問題のあるコード
async function riskyOperation() {
try {
const result = await someAsyncOperation();
return result;
} catch (error) {
// error は unknown 型(TypeScript 4.4以降)
console.log(error.message); // エラー: Property 'message' does not exist
}
}
// 解決策1:型ガードによるエラー処理
function isError(error: unknown): error is Error {
return error instanceof Error;
}
async function safeOperation(): Promise<string | null> {
try {
const result = await someAsyncOperation();
return result;
} catch (error) {
if (isError(error)) {
console.error('Error occurred:', error.message);
return null;
}
console.error('Unknown error:', error);
return null;
}
}
// 解決策2:カスタムエラー型の活用
class ApiError extends Error {
constructor(
message: string,
public status: number,
public response?: any
) {
super(message);
this.name = 'ApiError';
}
}
async function fetchWithErrorHandling<T>(
url: string
): Promise<T> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(
`HTTP Error: ${response.status}`,
response.status,
await response.text()
);
}
return response.json() as T;
} catch (error) {
if (error instanceof ApiError) {
console.error(
`API Error (${error.status}):`,
error.message
);
throw error;
} else if (isError(error)) {
console.error('Network Error:', error.message);
throw new ApiError('Network Error', 0, error.message);
} else {
console.error('Unknown Error:', error);
throw new ApiError('Unknown Error', 0, String(error));
}
}
}
エラー 4:非同期関数の条件分岐での型ナローイング失敗
条件分岐内で非同期処理を行う際に、型ナローイングが正しく機能しないケースです。
typescript// 問題のあるコード
type UserRole = 'admin' | 'user' | 'guest';
async function processUserAction(
userId: string,
role: UserRole
) {
if (role === 'admin') {
const adminData = await fetchAdminData(userId); // 型が曖昧
// adminData の型が正しく推論されない
} else {
const userData = await fetchUserData(userId); // 型が曖昧
// userData の型が正しく推論されない
}
}
// 解決策:型ガードと明示的な型注釈の組み合わせ
interface AdminData extends User {
permissions: string[];
lastLogin: Date;
}
interface RegularUserData extends User {
subscription: 'free' | 'premium';
}
async function processUserActionSafe(
userId: string,
role: UserRole
): Promise<AdminData | RegularUserData> {
if (role === 'admin') {
const adminData: AdminData = await fetchAdminData(
userId
);
// この分岐内では adminData は AdminData 型
return adminData;
} else {
const userData: RegularUserData = await fetchUserData(
userId
);
// この分岐内では userData は RegularUserData 型
return userData;
}
}
// さらに安全なアプローチ:判別可能ユニオン型
type ProcessedUser =
| { type: 'admin'; data: AdminData }
| { type: 'user'; data: RegularUserData };
async function processUserWithDiscrimination(
userId: string,
role: UserRole
): Promise<ProcessedUser> {
if (role === 'admin') {
const adminData = await fetchAdminData(userId);
return { type: 'admin', data: adminData };
} else {
const userData = await fetchUserData(userId);
return { type: 'user', data: userData };
}
}
エラー 5:非同期ジェネレーターの型定義不備
async/await と組み合わせた非同期ジェネレーターで型エラーが発生するケースです。
typescript// 問題のあるコード
async function* fetchUsersPaginated(pageSize: number) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`/api/users?page=${page}&size=${pageSize}`
);
const data = await response.json(); // any型
yield data.users; // 型が不明
hasMore = data.hasMore;
page++;
}
}
// 解決策:適切な型定義
interface PaginatedResponse<T> {
data: T[];
hasMore: boolean;
currentPage: number;
totalCount: number;
}
async function* fetchUsersPaginatedSafe(
pageSize: number
): AsyncGenerator<User[], void, unknown> {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`/api/users?page=${page}&size=${pageSize}`
);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data: PaginatedResponse<User> =
await response.json();
yield data.data; // User[] 型で安全
hasMore = data.hasMore;
page++;
}
}
// 使用例
async function processAllUsers() {
const userGenerator = fetchUsersPaginatedSafe(10);
for await (const users of userGenerator) {
// users は User[] 型として扱える
users.forEach((user) => {
console.log(`Processing user: ${user.name}`);
});
}
}
async/await の型安全な書き方
async/await 構文における型安全性を確保するための実践的なパターンを詳しく解説します。
関数の型注釈パターン
typescript// 基本的なasync関数の型注釈
async function fetchUserById(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json() as User;
}
// オプション引数を持つasync関数
async function fetchUsers(options?: {
limit?: number;
offset?: number;
sortBy?: 'name' | 'email' | 'createdAt';
order?: 'asc' | 'desc';
}): Promise<User[]> {
const params = new URLSearchParams();
if (options?.limit)
params.set('limit', options.limit.toString());
if (options?.offset)
params.set('offset', options.offset.toString());
if (options?.sortBy) params.set('sortBy', options.sortBy);
if (options?.order) params.set('order', options.order);
const response = await fetch(`/api/users?${params}`);
return response.json() as User[];
}
// 複雑な戻り値型を持つasync関数
interface UserWithMetadata {
user: User;
lastLoginAt: Date;
loginCount: number;
preferences: UserPreferences;
}
async function fetchUserWithMetadata(
userId: string
): Promise<UserWithMetadata> {
const [user, metadata, preferences] = await Promise.all([
fetchUser(userId),
fetchUserMetadata(userId),
fetchUserPreferences(userId),
]);
return {
user,
lastLoginAt: new Date(metadata.lastLoginAt),
loginCount: metadata.loginCount,
preferences,
};
}
// 条件付きで型が変わるasync関数
async function fetchUserData<T extends boolean>(
userId: string,
includePrivate: T
): Promise<
T extends true ? PrivateUserData : PublicUserData
> {
const response = await fetch(
`/api/users/${userId}?private=${includePrivate}`
);
return response.json() as T extends true
? PrivateUserData
: PublicUserData;
}
// 使用例
const publicData = await fetchUserData('123', false); // PublicUserData型
const privateData = await fetchUserData('123', true); // PrivateUserData型
戻り値型の自動推論と明示的指定
typescript// 自動推論が正しく機能するケース
async function simpleUserFetch(id: string) {
const response = await fetch(`/api/users/${id}`);
const user: User = await response.json();
return user; // Promise<User> と自動推論される
}
// 自動推論が失敗するケース
async function problematicFetch(id: string) {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Promise<any> になってしまう
}
// 明示的な型指定による解決
async function explicitTypeFetch(
id: string
): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json() as User;
}
// 複雑な条件分岐での型推論
async function conditionalFetch(
id: string,
version: 'v1' | 'v2'
): Promise<User | UserV2> {
if (version === 'v1') {
const response = await fetch(`/api/v1/users/${id}`);
return response.json() as User;
} else {
const response = await fetch(`/api/v2/users/${id}`);
return response.json() as UserV2;
}
}
// 型アサーション関数を使った安全なアプローチ
function assertUser(data: any): asserts data is User {
if (
!data ||
typeof data.id !== 'string' ||
typeof data.name !== 'string'
) {
throw new Error('Invalid user data');
}
}
async function safeFetchWithAssertion(
id: string
): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
assertUser(data); // この後、data は User 型として扱える
return data;
}
// ジェネリクス制約を使った柔軟な型設計
interface Identifiable {
id: string;
}
async function fetchEntity<T extends Identifiable>(
endpoint: string,
id: string,
validator: (data: any) => data is T
): Promise<T> {
const response = await fetch(`${endpoint}/${id}`);
const data = await response.json();
if (!validator(data)) {
throw new Error(`Invalid ${endpoint} data`);
}
return data;
}
// 使用例
const user = await fetchEntity('/api/users', '123', isUser);
const product = await fetchEntity(
'/api/products',
'456',
isProduct
);
エラー境界での型制御
typescript// try-catch文での型安全なエラーハンドリング
async function safeAsyncOperation(): Promise<User | null> {
try {
const user = await fetchUser('123');
const updatedUser = await updateUser(user.id, {
lastSeen: new Date(),
});
return updatedUser;
} catch (error) {
// TypeScript 4.4以降、catch節のerrorはunknown型
if (error instanceof ApiError) {
console.error(
`API Error: ${error.status} - ${error.message}`
);
return null;
} else if (error instanceof Error) {
console.error(`General Error: ${error.message}`);
return null;
} else {
console.error('Unknown error:', error);
return null;
}
}
}
// エラー型の細分化
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: any
) {
super(message);
this.name = 'ValidationError';
}
}
class NetworkError extends Error {
constructor(
message: string,
public originalError?: Error
) {
super(message);
this.name = 'NetworkError';
}
}
async function comprehensiveErrorHandling(): Promise<
Result<User, string>
> {
try {
const response = await fetch('/api/users/me');
if (!response.ok) {
throw new ApiError(
`HTTP ${response.status}`,
response.status
);
}
const userData = await response.json();
if (!isUser(userData)) {
throw new ValidationError(
'Invalid user data format',
'userData',
userData
);
}
return { success: true, data: userData };
} catch (error) {
let errorMessage: string;
if (error instanceof ApiError) {
errorMessage = `API Error (${error.status}): ${error.message}`;
} else if (error instanceof ValidationError) {
errorMessage = `Validation Error: ${error.message} for field '${error.field}'`;
} else if (error instanceof NetworkError) {
errorMessage = `Network Error: ${error.message}`;
} else if (error instanceof Error) {
errorMessage = `Unexpected Error: ${error.message}`;
} else {
errorMessage = `Unknown Error: ${String(error)}`;
}
return { success: false, error: errorMessage };
}
}
// async関数内でのPromise.catch()の活用
async function chainedErrorHandling(): Promise<User | null> {
return fetchUser('123')
.then((user) => updateUserLastSeen(user))
.catch((error: Error) => {
console.error(
'User operation failed:',
error.message
);
return null;
});
}
// 複数のasync操作でのエラー境界
async function batchOperationWithErrorBoundary(): Promise<{
successful: User[];
failed: string[];
}> {
const userIds = ['1', '2', '3', '4', '5'];
const results = await Promise.allSettled(
userIds.map((id) => fetchUser(id))
);
const successful: User[] = [];
const failed: string[] = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push(
`User ${userIds[index]}: ${result.reason.message}`
);
}
});
return { successful, failed };
}
複雑な非同期パターンの型定義
より高度な非同期処理パターンにおける型安全性を確保する方法を解説します。
Promise.all/race/allSettled の型安全な活用
typescript// Promise.allの型安全な使用
async function parallelDataFetch(userId: string): Promise<{
user: User;
posts: Post[];
comments: Comment[];
notifications: Notification[];
}> {
// 型が明確に推論される並行実行
const [user, posts, comments, notifications] =
await Promise.all([
fetchUser(userId) as Promise<User>,
fetchUserPosts(userId) as Promise<Post[]>,
fetchUserComments(userId) as Promise<Comment[]>,
fetchUserNotifications(userId) as Promise<
Notification[]
>,
]);
return { user, posts, comments, notifications };
}
// 異なる型の組み合わせでのPromise.all
async function mixedDataFetch(): Promise<{
config: AppConfig;
user: User | null;
permissions: string[];
}> {
const [config, user, permissions] = await Promise.all([
loadAppConfig() as Promise<AppConfig>,
getCurrentUser() as Promise<User | null>,
fetchUserPermissions() as Promise<string[]>,
]);
return { config, user, permissions };
}
// Promise.raceの型安全な実装
async function timeoutFetch<T>(
promise: Promise<T>,
timeout: number
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(
() => reject(new Error('Operation timed out')),
timeout
);
});
return Promise.race([promise, timeoutPromise]);
}
// 使用例
const user = await timeoutFetch(fetchUser('123'), 5000); // 5秒でタイムアウト
// Promise.allSettledの型安全な活用
interface BatchResult<T> {
successful: T[];
failed: PromiseRejectedResult[];
}
async function batchProcess<T>(
promises: Promise<T>[]
): Promise<BatchResult<T>> {
const results = await Promise.allSettled(promises);
const successful: T[] = [];
const failed: PromiseRejectedResult[] = [];
results.forEach((result) => {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push(result);
}
});
return { successful, failed };
}
// 使用例:複数ユーザーの並行処理
async function processMultipleUsers(userIds: string[]) {
const userPromises = userIds.map((id) => fetchUser(id));
const result = await batchProcess(userPromises);
console.log(
`Successfully processed: ${result.successful.length} users`
);
console.log(
`Failed to process: ${result.failed.length} users`
);
return result.successful; // User[] 型
}
非同期ジェネレーターと AsyncIterable の型定義
typescript// 基本的な非同期ジェネレーター
async function* fetchUsersStream(): AsyncGenerator<
User,
void,
unknown
> {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`/api/users?page=${page}&limit=10`
);
const data: PaginatedResponse<User> =
await response.json();
for (const user of data.data) {
yield user; // User型を1つずつ生成
}
hasMore = data.hasMore;
page++;
}
}
// バッチ処理向けの非同期ジェネレーター
async function* fetchUsersBatch(
batchSize: number = 50
): AsyncGenerator<User[], void, unknown> {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`/api/users?page=${page}&limit=${batchSize}`
);
const data: PaginatedResponse<User> =
await response.json();
yield data.data; // User[]型をバッチで生成
hasMore = data.hasMore;
page++;
}
}
// エラーハンドリング付きの非同期ジェネレーター
async function* robustDataStream<T>(
fetchFn: (page: number) => Promise<PaginatedResponse<T>>,
maxRetries: number = 3
): AsyncGenerator<T, void, unknown> {
let page = 1;
let hasMore = true;
while (hasMore) {
let retries = 0;
while (retries < maxRetries) {
try {
const data = await fetchFn(page);
for (const item of data.data) {
yield item;
}
hasMore = data.hasMore;
page++;
break; // 成功したのでリトライループを抜ける
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw new Error(
`Failed to fetch page ${page} after ${maxRetries} retries: ${
error instanceof Error
? error.message
: String(error)
}`
);
}
// 指数バックオフでリトライ
await new Promise((resolve) =>
setTimeout(resolve, Math.pow(2, retries) * 1000)
);
}
}
}
}
// AsyncIterable インターフェースの実装
class UserIterator implements AsyncIterable<User> {
constructor(
private startId: string,
private batchSize: number = 10
) {}
async *[Symbol.asyncIterator](): AsyncGenerator<
User,
void,
unknown
> {
let currentId = this.startId;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`/api/users?after=${currentId}&limit=${this.batchSize}`
);
const data: {
users: User[];
hasMore: boolean;
nextId?: string;
} = await response.json();
for (const user of data.users) {
yield user;
}
hasMore = data.hasMore;
if (data.nextId) {
currentId = data.nextId;
}
}
}
}
// 使用例
async function processAllUsers() {
const userIterator = new UserIterator('0');
for await (const user of userIterator) {
console.log(`Processing user: ${user.name}`);
// 各ユーザーの処理...
}
}
// 複雑な非同期イテレーター
interface StreamOptions {
batchSize?: number;
filter?: (item: User) => boolean;
transform?: (item: User) => User;
}
async function* createUserStream(
options: StreamOptions = {}
): AsyncGenerator<User, void, unknown> {
const {
batchSize = 10,
filter = () => true,
transform = (user) => user,
} = options;
for await (const userBatch of fetchUsersBatch(
batchSize
)) {
for (const user of userBatch) {
if (filter(user)) {
yield transform(user);
}
}
}
}
// 高度な使用例:フィルタリングと変換
async function processActiveUsers() {
const activeUserStream = createUserStream({
batchSize: 20,
filter: (user) =>
user.isActive && user.email.includes('@'),
transform: (user) => ({
...user,
displayName: `${user.name} (${user.email})`,
}),
});
for await (const user of activeUserStream) {
console.log(
`Processing active user: ${user.displayName}`
);
}
}
並行処理とキャンセレーション機能の型設計
typescript// AbortController を活用したキャンセル可能な非同期処理
interface CancellablePromise<T> extends Promise<T> {
cancel(): void;
}
function createCancellablePromise<T>(
executor: (
resolve: (value: T) => void,
reject: (reason?: any) => void,
signal: AbortSignal
) => void
): CancellablePromise<T> {
const controller = new AbortController();
const promise = new Promise<T>((resolve, reject) => {
executor(resolve, reject, controller.signal);
controller.signal.addEventListener('abort', () => {
reject(new Error('Operation was cancelled'));
});
}) as CancellablePromise<T>;
promise.cancel = () => controller.abort();
return promise;
}
// キャンセル可能な fetch 関数
function cancellableFetch<T>(
url: string,
options?: RequestInit
): CancellablePromise<T> {
return createCancellablePromise<T>(
(resolve, reject, signal) => {
fetch(url, { ...options, signal })
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(resolve)
.catch(reject);
}
);
}
// 並行処理マネージャー
class ConcurrencyManager<T> {
private running = new Set<Promise<T>>();
private maxConcurrent: number;
constructor(maxConcurrent: number = 3) {
this.maxConcurrent = maxConcurrent;
}
async execute<R>(task: () => Promise<R>): Promise<R> {
while (this.running.size >= this.maxConcurrent) {
await Promise.race(this.running);
}
const promise = task().finally(() => {
this.running.delete(promise as any);
});
this.running.add(promise as any);
return promise;
}
async waitAll(): Promise<void> {
await Promise.all(this.running);
}
}
// 使用例:並行制御付きのデータ処理
async function processUsersWithConcurrencyControl(
userIds: string[]
) {
const manager = new ConcurrencyManager<User>(5); // 最大5並行
const userPromises = userIds.map((id) =>
manager.execute(() => fetchUser(id))
);
const users = await Promise.all(userPromises);
return users;
}
// タイムアウト付きの並行処理
interface TimedResult<T> {
value?: T;
error?: Error;
timedOut: boolean;
duration: number;
}
async function executeWithTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<TimedResult<T>> {
const startTime = Date.now();
try {
const result = await Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error('Timeout')),
timeoutMs
)
),
]);
return {
value: result,
timedOut: false,
duration: Date.now() - startTime,
};
} catch (error) {
const isTimeout =
error instanceof Error && error.message === 'Timeout';
return {
error:
error instanceof Error
? error
: new Error(String(error)),
timedOut: isTimeout,
duration: Date.now() - startTime,
};
}
}
// 使用例
async function fetchWithTimeoutHandling(userId: string) {
const result = await executeWithTimeout(
fetchUser(userId),
3000
);
if (result.timedOut) {
console.warn(
`User fetch timed out after ${result.duration}ms`
);
return null;
} else if (result.error) {
console.error(
`User fetch failed: ${result.error.message}`
);
return null;
} else {
console.log(`User fetched in ${result.duration}ms`);
return result.value;
}
}
実際のプロジェクトでの実装パターン
実際のプロジェクトで活用できる、実践的な非同期処理の型設計パターンを紹介します。
API クライアントライブラリの型設計
typescript// 基本的なAPIクライアントの型設計
interface ApiClientConfig {
baseUrl: string;
timeout: number;
retries: number;
headers?: Record<string, string>;
}
interface ApiResponse<T> {
data: T;
status: number;
headers: Record<string, string>;
}
class TypeSafeApiClient {
private config: ApiClientConfig;
constructor(config: ApiClientConfig) {
this.config = config;
}
async get<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>('GET', endpoint);
}
async post<T, U = any>(
endpoint: string,
data: U
): Promise<ApiResponse<T>> {
return this.request<T>('POST', endpoint, data);
}
async put<T, U = any>(
endpoint: string,
data: U
): Promise<ApiResponse<T>> {
return this.request<T>('PUT', endpoint, data);
}
async delete<T>(
endpoint: string
): Promise<ApiResponse<T>> {
return this.request<T>('DELETE', endpoint);
}
private async request<T>(
method: string,
endpoint: string,
data?: any
): Promise<ApiResponse<T>> {
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
this.config.timeout
);
try {
const response = await fetch(
`${this.config.baseUrl}${endpoint}`,
{
method,
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: data ? JSON.stringify(data) : undefined,
signal: controller.signal,
}
);
clearTimeout(timeoutId);
if (!response.ok) {
throw new ApiError(
`HTTP ${response.status}`,
response.status,
await response.text()
);
}
const responseData = await response.json();
return {
data: responseData as T,
status: response.status,
headers: Object.fromEntries(
response.headers.entries()
),
};
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
}
// 特定のドメインに特化したAPIクライアント
interface UserCreateRequest {
name: string;
email: string;
role: 'admin' | 'user';
}
interface UserUpdateRequest {
name?: string;
email?: string;
isActive?: boolean;
}
class UserApiClient {
constructor(private apiClient: TypeSafeApiClient) {}
async getUser(id: string): Promise<User> {
const response = await this.apiClient.get<User>(
`/users/${id}`
);
return response.data;
}
async getUsers(params?: {
page?: number;
limit?: number;
search?: string;
}): Promise<PaginatedResponse<User>> {
const queryString = params
? '?' +
new URLSearchParams(
Object.entries(params).map(([k, v]) => [
k,
String(v),
])
).toString()
: '';
const response = await this.apiClient.get<
PaginatedResponse<User>
>(`/users${queryString}`);
return response.data;
}
async createUser(
userData: UserCreateRequest
): Promise<User> {
const response = await this.apiClient.post<
User,
UserCreateRequest
>('/users', userData);
return response.data;
}
async updateUser(
id: string,
userData: UserUpdateRequest
): Promise<User> {
const response = await this.apiClient.put<
User,
UserUpdateRequest
>(`/users/${id}`, userData);
return response.data;
}
async deleteUser(id: string): Promise<void> {
await this.apiClient.delete<void>(`/users/${id}`);
}
}
React/Vue.js での非同期状態管理
typescript// React でのカスタムフック型設計
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
interface UseAsyncResult<T> extends AsyncState<T> {
execute: () => Promise<void>;
reset: () => void;
}
function useAsync<T>(
asyncFn: () => Promise<T>,
deps: React.DependencyList = []
): UseAsyncResult<T> {
const [state, setState] = React.useState<AsyncState<T>>({
data: null,
loading: false,
error: null,
});
const execute = React.useCallback(async () => {
setState((prev) => ({
...prev,
loading: true,
error: null,
}));
try {
const result = await asyncFn();
setState({
data: result,
loading: false,
error: null,
});
} catch (error) {
setState({
data: null,
loading: false,
error:
error instanceof Error
? error
: new Error(String(error)),
});
}
}, deps);
const reset = React.useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
React.useEffect(() => {
execute();
}, [execute]);
return { ...state, execute, reset };
}
// 使用例
function UserProfile({ userId }: { userId: string }) {
const {
data: user,
loading,
error,
execute,
} = useAsync(() => fetchUser(userId), [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={execute}>Refresh</button>
</div>
);
}
// Vue.js での Composition API 型設計
interface AsyncComposable<T> {
data: Ref<T | null>;
loading: Ref<boolean>;
error: Ref<Error | null>;
execute: () => Promise<void>;
reset: () => void;
}
function useAsyncData<T>(
asyncFn: () => Promise<T>,
options: {
immediate?: boolean;
resetOnExecute?: boolean;
} = {}
): AsyncComposable<T> {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
const execute = async () => {
if (options.resetOnExecute) {
data.value = null;
error.value = null;
}
loading.value = true;
try {
const result = await asyncFn();
data.value = result;
error.value = null;
} catch (err) {
error.value =
err instanceof Error ? err : new Error(String(err));
data.value = null;
} finally {
loading.value = false;
}
};
const reset = () => {
data.value = null;
loading.value = false;
error.value = null;
};
if (options.immediate !== false) {
onMounted(execute);
}
return { data, loading, error, execute, reset };
}
// Vue コンポーネントでの使用例
export default defineComponent({
props: {
userId: {
type: String,
required: true,
},
},
setup(props) {
const {
data: user,
loading,
error,
execute,
} = useAsyncData(() => fetchUser(props.userId), {
immediate: true,
});
watch(() => props.userId, execute);
return {
user,
loading,
error,
refreshUser: execute,
};
},
});
Node.js サーバーサイドでの非同期処理パターン
typescript// Express.js でのミドルウェア型設計
interface AuthenticatedRequest extends Request {
user: User;
}
type AsyncRequestHandler<T = Request> = (
req: T,
res: Response,
next: NextFunction
) => Promise<void>;
const asyncHandler = <T = Request>(
fn: AsyncRequestHandler<T>
) => {
return (req: T, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// 認証ミドルウェア
const authenticate: AsyncRequestHandler = async (
req,
res,
next
) => {
try {
const token = req.headers.authorization?.replace(
'Bearer ',
''
);
if (!token) {
res.status(401).json({ error: 'No token provided' });
return;
}
const user = await verifyToken(token);
(req as AuthenticatedRequest).user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// 型安全なルートハンドラー
const getUserProfile = asyncHandler<AuthenticatedRequest>(
async (req, res) => {
const userId = req.params.id;
const currentUser = req.user;
// 権限チェック
if (
userId !== currentUser.id &&
currentUser.role !== 'admin'
) {
res.status(403).json({ error: 'Access denied' });
return;
}
const user = await fetchUser(userId);
res.json({ user });
}
);
// データベース操作の型安全パターン
interface DatabaseConnection {
query<T>(sql: string, params?: any[]): Promise<T[]>;
transaction<T>(
callback: (connection: DatabaseConnection) => Promise<T>
): Promise<T>;
}
class UserRepository {
constructor(private db: DatabaseConnection) {}
async findById(id: string): Promise<User | null> {
const users = await this.db.query<User>(
'SELECT * FROM users WHERE id = ?',
[id]
);
return users[0] || null;
}
async create(userData: UserCreateRequest): Promise<User> {
return this.db.transaction(async (connection) => {
// ユーザー作成
const [user] = await connection.query<User>(
'INSERT INTO users (name, email, role) VALUES (?, ?, ?) RETURNING *',
[userData.name, userData.email, userData.role]
);
// 初期設定作成
await connection.query(
'INSERT INTO user_settings (user_id, theme, notifications) VALUES (?, ?, ?)',
[user.id, 'light', true]
);
return user;
});
}
async update(
id: string,
userData: UserUpdateRequest
): Promise<User | null> {
const updates: string[] = [];
const values: any[] = [];
if (userData.name !== undefined) {
updates.push('name = ?');
values.push(userData.name);
}
if (userData.email !== undefined) {
updates.push('email = ?');
values.push(userData.email);
}
if (userData.isActive !== undefined) {
updates.push('is_active = ?');
values.push(userData.isActive);
}
if (updates.length === 0) {
return this.findById(id);
}
values.push(id);
const [user] = await this.db.query<User>(
`UPDATE users SET ${updates.join(
', '
)} WHERE id = ? RETURNING *`,
values
);
return user || null;
}
}
// バックグラウンドジョブの型設計
interface JobProcessor<T> {
process(data: T): Promise<void>;
}
interface Job<T> {
id: string;
type: string;
data: T;
priority: number;
createdAt: Date;
attempts: number;
maxAttempts: number;
}
class JobQueue<T> {
private processors = new Map<string, JobProcessor<any>>();
private running = false;
register<U>(type: string, processor: JobProcessor<U>) {
this.processors.set(type, processor);
}
async enqueue<U>(
type: string,
data: U,
options: {
priority?: number;
maxAttempts?: number;
} = {}
): Promise<void> {
const job: Job<U> = {
id: generateId(),
type,
data,
priority: options.priority || 0,
createdAt: new Date(),
attempts: 0,
maxAttempts: options.maxAttempts || 3,
};
await this.saveJob(job);
}
async start(): Promise<void> {
this.running = true;
while (this.running) {
try {
const job = await this.getNextJob();
if (job) {
await this.processJob(job);
} else {
await new Promise((resolve) =>
setTimeout(resolve, 1000)
);
}
} catch (error) {
console.error('Job processing error:', error);
}
}
}
private async processJob<U>(job: Job<U>): Promise<void> {
const processor = this.processors.get(job.type);
if (!processor) {
throw new Error(
`No processor found for job type: ${job.type}`
);
}
try {
await processor.process(job.data);
await this.markJobComplete(job.id);
} catch (error) {
await this.handleJobError(job, error);
}
}
private async handleJobError<U>(
job: Job<U>,
error: any
): Promise<void> {
job.attempts++;
if (job.attempts >= job.maxAttempts) {
await this.markJobFailed(job.id, error);
} else {
// 指数バックオフでリトライ
const delay = Math.pow(2, job.attempts) * 1000;
setTimeout(() => this.retryJob(job), delay);
}
}
private async saveJob<U>(job: Job<U>): Promise<void> {
// データベースに保存
}
private async getNextJob(): Promise<Job<any> | null> {
// 優先度順でジョブを取得
return null;
}
private async markJobComplete(
jobId: string
): Promise<void> {
// ジョブを完了としてマーク
}
private async markJobFailed(
jobId: string,
error: any
): Promise<void> {
// ジョブを失敗としてマーク
}
private async retryJob<U>(job: Job<U>): Promise<void> {
// ジョブを再実行キューに追加
}
}
function generateId(): string {
return Math.random().toString(36).substring(2);
}
まとめ
TypeScript における非同期処理の型安全性を確保することは、現代の Web アプリケーション開発において不可欠なスキルです。本記事で紹介した内容を実践することで、以下のメリットを得ることができます。
型安全な非同期処理による効果
# | 効果項目 | 改善度合い | 実装コスト | 維持コスト |
---|---|---|---|---|
1 | 実行時エラーの削減 | 高 | 中 | 低 |
2 | 開発効率の向上 | 高 | 低 | 低 |
3 | コードの保守性向上 | 高 | 中 | 低 |
4 | チーム開発の生産性向上 | 中 | 中 | 低 |
5 | リファクタリングの安全性 | 高 | 低 | 低 |
実践で重要なポイント
- 明示的な型注釈: Promise の戻り値型は必ず明示的に指定する
- エラーハンドリング: try-catch での型安全なエラー処理を心がける
- 型ガードの活用: 外部 API からのデータは型ガードで検証する
- ジェネリクスの活用: 再利用可能で型安全な関数を設計する
- 適切な抽象化: プロジェクトに応じた適切なレベルで抽象化を行う
TypeScript の型システムと非同期処理を適切に組み合わせることで、バグの少ない堅牢なアプリケーションを効率的に開発することができます。今回紹介したパターンを実際のプロジェクトで少しずつ取り入れていき、型安全で快適な非同期処理開発を体験してください。
関連リンク
- article
TypeScript 非同期処理の型安全な書き方:Promise・async/await の型定義
- article
Zustandでの非同期処理とfetch連携パターン(パターン 7: リクエストのキャンセルと競合状態の管理)
- article
Zustandでの非同期処理とfetch連携パターン(パターン 6: キャッシュとデータ永続化)
- article
Zustandでの非同期処理とfetch連携パターン(パターン 5: リクエストの依存関係と連鎖)
- article
Zustandでの非同期処理とfetch連携パターン(パターン 4: WebSocket とリアルタイム更新)
- article
Zustandでの非同期処理とfetch連携パターン(パターン 3: 無限スクロールとページネーション)
- review
もう無駄な努力はしない!『イシューからはじめよ』安宅和人著で身につけた、99%の人が知らない本当に価値ある問題の見つけ方
- review
もう朝起きるのが辛くない!『スタンフォード式 最高の睡眠』西野精治著で学んだ、たった 90 分で人生が変わる睡眠革命
- review
もう「なんとなく」で決めない!『解像度を上げる』馬田隆明著で身につけた、曖昧思考を一瞬で明晰にする技術
- review
もう疲れ知らず!『最高の体調』鈴木祐著で手に入れた、一生モノの健康習慣術
- review
人生が激変!『苦しかったときの話をしようか』森岡毅著で発見した、本当に幸せなキャリアの築き方
- review
もう「何言ってるの?」とは言わせない!『バナナの魅力を 100 文字で伝えてください』柿内尚文著 で今日からあなたも伝え方の達人!