はじめに#
JavaScriptの非同期処理は、コールバック地獄からPromise、そしてasync/awaitへと進化してきました。TypeScriptを使えば、この非同期処理に型安全性を加えることができます。しかし、Promiseの型定義やasync関数の戻り値の型推論、複数のPromiseを組み合わせたときの型の扱いなど、理解すべきポイントは少なくありません。
本記事では、TypeScriptにおける非同期処理の型付けを体系的に解説します。Promiseの型パラメータの意味から、async/awaitでの型推論の仕組み、Promise.allやPromise.raceなどのユーティリティメソッドの型、さらにAbortControllerを使ったキャンセル処理や非同期イテレータまで、実践的な内容をカバーします。
この記事を読み終えると、以下のことができるようになります。
Promise<T>の型パラメータを正しく理解し、適切に型定義できる
- async/await構文での型推論の仕組みを理解し、活用できる
Promise.all、Promise.race、Promise.allSettledの型を正確に扱える
- AbortControllerを使った型安全なキャンセル処理を実装できる
- 非同期イテレータ(
for await...of)を型安全に使用できる
実行環境・前提条件#
前提知識#
- TypeScriptの基本的な型注釈(基本型入門を参照)
- ジェネリクスの基本(ジェネリクス入門を参照)
- JavaScriptのPromise、async/awaitの基本構文
動作確認環境#
| ツール |
バージョン |
| Node.js |
20.x以上 |
| TypeScript |
5.7以上 |
| VS Code |
最新版 |
本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。
期待される結果#
本記事のコードを実行すると、以下の動作を確認できます。
- Promiseの成功値が正しい型として推論される
- async関数の戻り値が
Promise<T>として型付けされる
- 複数のPromiseを組み合わせた結果がタプル型として推論される
- AbortSignalを受け取る関数が型安全にキャンセル処理を行える
TypeScriptにおけるPromiseの型定義#
Promise型の基本構造#
Promiseは、非同期処理の結果を表すオブジェクトです。TypeScriptでは、Promise<T>というジェネリック型として定義されており、Tは非同期処理が成功したときに返される値の型を表します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Promise<T>の型定義(簡略化)
interface Promise<T> {
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2>;
catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult>;
finally(onfinally?: (() => void) | null): Promise<T>;
}
|
この型定義から、以下のことがわかります。
Tは成功時の値の型: Promise<string>なら、成功時にstring型の値が得られる
thenのチェーンで型が変わる: thenに渡す関数の戻り値によって、次のPromiseの型が決まる
- エラーの型は
any: catchやonrejectedで受け取るエラーの型はany(厳密にはunknownが推奨)
flowchart LR
A["Promise<T>"] --> B{状態}
B -->|fulfilled| C["value: T"]
B -->|rejected| D["reason: any"]
B -->|pending| E["待機中"]Promiseの作成と型付け#
Promiseを作成する際、コンストラクタに渡すexecutor関数のresolve引数の型から、Promise全体の型が推論されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 型パラメータを明示する場合
const promise1: Promise<string> = new Promise((resolve, reject) => {
resolve("成功");
// resolve(123); // エラー: Argument of type 'number' is not assignable to parameter of type 'string'
});
// 型推論に任せる場合
const promise2 = new Promise<number>((resolve, reject) => {
setTimeout(() => {
resolve(42);
}, 1000);
});
// promise2の型: Promise<number>
// resolveの引数から推論される場合
const promise3 = new Promise((resolve) => {
resolve("hello");
});
// promise3の型: Promise<unknown> - 型パラメータを指定しないとunknownになる
|
明示的に型パラメータを指定することで、resolveに渡す値の型が正しいかをコンパイル時にチェックできます。
実用的なPromise関数の型定義#
API呼び出しなど、実際のアプリケーションでよく使うPromiseを返す関数の型定義を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// ユーザー情報の型
interface User {
id: number;
name: string;
email: string;
}
// APIレスポンスの型
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// ユーザーを取得する非同期関数
function fetchUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
// 実際にはfetchなどでAPI呼び出し
setTimeout(() => {
if (id > 0) {
resolve({
id,
name: "田中太郎",
email: "tanaka@example.com"
});
} else {
reject(new Error("Invalid user ID"));
}
}, 100);
});
}
// 使用例
async function main() {
const user = await fetchUser(1);
// userの型: User
console.log(user.name); // "田中太郎"
}
|
関数のシグネチャでPromise<User>と明示することで、呼び出し側はawaitした結果がUser型であることを保証されます。
async/awaitの型推論#
async関数の戻り値の型#
asyncキーワードを付けた関数は、必ずPromiseを返します。TypeScriptはこれを自動的に推論します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 戻り値の型が自動的にPromise<string>と推論される
async function getMessage(): Promise<string> {
return "Hello, TypeScript!";
}
// 型注釈を省略しても推論される
async function getNumber() {
return 42;
}
// getNumberの戻り値の型: Promise<number>
// 複数の戻り値パターンがある場合
async function getValue(condition: boolean) {
if (condition) {
return "文字列";
}
return 123;
}
// getValueの戻り値の型: Promise<string | number>
|
awaitと型の絞り込み#
await式は、Promise<T>からTを取り出します。この型の「アンラップ」はTypeScriptによって自動的に行われます。
1
2
3
4
5
6
7
8
9
|
async function processData() {
const promise: Promise<string> = Promise.resolve("データ");
// awaitでPromise<string>からstringを取り出す
const data: string = await promise;
// 以降はstring型として扱える
console.log(data.toUpperCase()); // "データ"
}
|
ネストしたPromiseも正しくアンラップされます。
1
2
3
4
5
6
7
|
async function nestedPromise() {
const nested: Promise<Promise<number>> = Promise.resolve(Promise.resolve(42));
// awaitは再帰的にPromiseをアンラップする
const value = await nested;
// valueの型: number(Promise<number>ではない)
}
|
条件分岐とawait#
awaitを条件分岐と組み合わせると、型の絞り込みが行われます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
interface SuccessResponse {
success: true;
data: string;
}
interface ErrorResponse {
success: false;
error: string;
}
type ApiResult = SuccessResponse | ErrorResponse;
async function callApi(): Promise<ApiResult> {
// APIの呼び出し結果をシミュレート
return { success: true, data: "結果データ" };
}
async function handleApiResult() {
const result = await callApi();
// resultの型: ApiResult (SuccessResponse | ErrorResponse)
if (result.success) {
// ここではSuccessResponse型に絞り込まれる
console.log(result.data); // OK: data プロパティにアクセス可能
} else {
// ここではErrorResponse型に絞り込まれる
console.log(result.error); // OK: error プロパティにアクセス可能
}
}
|
Promise.all/race/allSettledの型#
複数のPromiseを組み合わせるユーティリティメソッドは、それぞれ異なる型を返します。TypeScriptはこれらを正確に推論します。
Promise.allの型#
Promise.allは、すべてのPromiseが成功したときに、結果をタプルまたは配列として返します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
async function fetchMultipleData() {
const userPromise: Promise<{ name: string }> = Promise.resolve({ name: "太郎" });
const scorePromise: Promise<number> = Promise.resolve(100);
const tagsPromise: Promise<string[]> = Promise.resolve(["TypeScript", "Promise"]);
// Promise.allの結果はタプル型として推論される
const results = await Promise.all([userPromise, scorePromise, tagsPromise]);
// resultsの型: [{ name: string }, number, string[]]
const [user, score, tags] = results;
// user: { name: string }
// score: number
// tags: string[]
console.log(user.name, score, tags.join(", "));
}
|
配列リテラルを渡す場合、TypeScriptはタプル型として推論します。これにより、各要素の型が保持されます。
1
2
3
4
5
6
7
8
9
10
11
|
// 可変長の配列を渡す場合
async function fetchUserScores(userIds: number[]) {
const promises = userIds.map(id =>
Promise.resolve({ id, score: Math.random() * 100 })
);
const results = await Promise.all(promises);
// resultsの型: { id: number; score: number }[]
return results;
}
|
Promise.raceの型#
Promise.raceは、最初に完了したPromiseの結果を返します。結果の型は、渡されたPromiseの型のユニオンになります。
1
2
3
4
5
6
7
8
9
10
11
12
|
async function raceExample() {
const fast: Promise<string> = new Promise(resolve =>
setTimeout(() => resolve("速い"), 100)
);
const slow: Promise<number> = new Promise(resolve =>
setTimeout(() => resolve(42), 1000)
);
const result = await Promise.race([fast, slow]);
// resultの型: string | number
// どちらが先に完了するかは実行時まで不明なため、ユニオン型になる
}
|
タイムアウト処理の実装でよく使われます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
function timeout<T>(ms: number): Promise<never> {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
}
async function fetchWithTimeout<T>(
promise: Promise<T>,
ms: number
): Promise<T> {
// Promise.raceでタイムアウトを実装
// timeout()はPromise<never>なので、結果の型はTになる
return Promise.race([promise, timeout(ms)]);
}
// 使用例
async function main() {
try {
const result = await fetchWithTimeout(
fetch("https://api.example.com/data").then(r => r.json()),
5000
);
console.log(result);
} catch (error) {
console.error("タイムアウトまたはエラー:", error);
}
}
|
Promise.allSettledの型#
Promise.allSettledは、すべてのPromiseが完了(成功または失敗)するまで待ち、各Promiseの結果をPromiseSettledResultとして返します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// PromiseSettledResultの型定義
type PromiseSettledResult<T> =
| PromiseFulfilledResult<T>
| PromiseRejectedResult;
interface PromiseFulfilledResult<T> {
status: "fulfilled";
value: T;
}
interface PromiseRejectedResult {
status: "rejected";
reason: any;
}
|
実際の使用例を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
async function allSettledExample() {
const promises = [
Promise.resolve("成功1"),
Promise.reject(new Error("失敗")),
Promise.resolve("成功2")
];
const results = await Promise.allSettled(promises);
// resultsの型: PromiseSettledResult<string>[]
for (const result of results) {
if (result.status === "fulfilled") {
// PromiseFulfilledResult<string>に絞り込まれる
console.log("成功:", result.value);
} else {
// PromiseRejectedResultに絞り込まれる
console.log("失敗:", result.reason);
}
}
}
|
Promise.allSettledは、一部が失敗しても他の結果を取得したい場合に便利です。型ガードを使って成功・失敗を安全に判別できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 成功した結果のみを抽出するヘルパー関数
function filterFulfilled<T>(
results: PromiseSettledResult<T>[]
): T[] {
return results
.filter((r): r is PromiseFulfilledResult<T> => r.status === "fulfilled")
.map(r => r.value);
}
// 使用例
async function fetchAllUsers(ids: number[]) {
const promises = ids.map(id => fetchUser(id));
const results = await Promise.allSettled(promises);
const successfulUsers = filterFulfilled(results);
// successfulUsersの型: User[]
return successfulUsers;
}
|
Promise.anyの型#
Promise.anyはES2021で追加された機能で、最初に成功したPromiseの結果を返します。すべてが失敗した場合はAggregateErrorをスローします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
async function anyExample() {
const promises = [
fetch("https://api1.example.com/data"),
fetch("https://api2.example.com/data"),
fetch("https://api3.example.com/data")
];
try {
// 最初に成功したレスポンスを取得
const response = await Promise.any(promises);
// responseの型: Response
console.log("成功:", response.url);
} catch (error) {
// すべて失敗した場合
if (error instanceof AggregateError) {
console.log("すべて失敗:", error.errors);
}
}
}
|
AbortControllerと型安全なキャンセル処理#
AbortControllerの基本#
AbortControllerは、非同期処理をキャンセルするための標準APIです。TypeScriptでは、AbortControllerとAbortSignalの型が組み込みで定義されています。
1
2
3
4
5
6
7
8
9
10
11
|
// AbortControllerの基本的な使い方
const controller = new AbortController();
// controller.signalの型: AbortSignal
// キャンセルをトリガー
controller.abort();
// controller.abort("カスタム理由"); // 理由を指定することも可能
// シグナルの状態を確認
console.log(controller.signal.aborted); // true
console.log(controller.signal.reason); // "AbortError" または指定した理由
|
fetchでのAbortSignalの使用#
fetch関数は、AbortSignalを受け取ってキャンセル可能なリクエストを作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
async function fetchWithAbort(url: string, signal?: AbortSignal): Promise<Response> {
const response = await fetch(url, { signal });
return response;
}
// 使用例
async function main() {
const controller = new AbortController();
// 3秒後にキャンセル
setTimeout(() => controller.abort(), 3000);
try {
const response = await fetchWithAbort(
"https://api.example.com/slow-endpoint",
controller.signal
);
const data = await response.json();
console.log(data);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.log("リクエストがキャンセルされました");
} else {
throw error;
}
}
}
|
型安全なキャンセル可能関数の設計#
カスタムの非同期関数でもAbortSignalを活用できます。型を明示することで、キャンセル処理の意図が明確になります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
interface CancellableOptions {
signal?: AbortSignal;
}
interface ProcessResult {
data: string;
processedAt: Date;
}
async function processWithCancellation(
input: string,
options: CancellableOptions = {}
): Promise<ProcessResult> {
const { signal } = options;
// キャンセル済みかチェック
if (signal?.aborted) {
throw new DOMException("処理がキャンセルされました", "AbortError");
}
// キャンセルイベントのリスナーを設定
return new Promise((resolve, reject) => {
const abortHandler = () => {
reject(new DOMException("処理がキャンセルされました", "AbortError"));
};
signal?.addEventListener("abort", abortHandler);
// 重い処理をシミュレート
setTimeout(() => {
signal?.removeEventListener("abort", abortHandler);
resolve({
data: input.toUpperCase(),
processedAt: new Date()
});
}, 2000);
});
}
// 使用例
async function example() {
const controller = new AbortController();
// 1秒後にキャンセル
setTimeout(() => controller.abort(), 1000);
try {
const result = await processWithCancellation("hello", {
signal: controller.signal
});
console.log(result);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
console.log("処理がキャンセルされました");
}
}
}
|
AbortSignal.timeoutの活用#
TypeScript 5.7以降(ES2024対応)では、AbortSignal.timeoutを使った簡潔なタイムアウト処理が可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
async function fetchWithBuiltinTimeout() {
try {
// 5秒でタイムアウトするシグナルを作成
const response = await fetch("https://api.example.com/data", {
signal: AbortSignal.timeout(5000)
});
return await response.json();
} catch (error) {
if (error instanceof Error && error.name === "TimeoutError") {
console.log("タイムアウトしました");
}
throw error;
}
}
|
AbortSignal.anyによる複合条件#
複数のキャンセル条件を組み合わせることも可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
async function fetchWithMultipleConditions() {
const userController = new AbortController();
// ユーザーのキャンセルまたは10秒のタイムアウト
const combinedSignal = AbortSignal.any([
userController.signal,
AbortSignal.timeout(10000)
]);
try {
const response = await fetch("https://api.example.com/data", {
signal: combinedSignal
});
return await response.json();
} catch (error) {
console.log("キャンセルまたはタイムアウト");
throw error;
}
}
|
非同期イテレータの型定義#
AsyncIteratorとAsyncIterableの基本#
非同期イテレータは、for await...ofループで順次データを取得する仕組みです。TypeScriptでは、AsyncIteratorとAsyncIterableの型が定義されています。
1
2
3
4
5
6
7
8
9
10
11
|
// AsyncIterableの型定義(簡略化)
interface AsyncIterable<T> {
[Symbol.asyncIterator](): AsyncIterator<T>;
}
// AsyncIteratorの型定義(簡略化)
interface AsyncIterator<T, TReturn = any, TNext = undefined> {
next(...args: [] | [TNext]): Promise<IteratorResult<T, TReturn>>;
return?(value?: TReturn | PromiseLike<TReturn>): Promise<IteratorResult<T, TReturn>>;
throw?(e?: any): Promise<IteratorResult<T, TReturn>>;
}
|
async generatorの型定義#
async function*構文を使って非同期ジェネレータを定義できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 非同期ジェネレータ関数
async function* generateNumbers(
start: number,
end: number
): AsyncGenerator<number, void, undefined> {
for (let i = start; i <= end; i++) {
// 非同期処理をシミュレート
await new Promise(resolve => setTimeout(resolve, 100));
yield i;
}
}
// 使用例
async function main() {
for await (const num of generateNumbers(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5 が順次出力
}
}
|
AsyncGenerator<T, TReturn, TNext>の型パラメータは以下の意味を持ちます。
T: yieldで生成される値の型
TReturn: ジェネレータが終了時に返す値の型
TNext: next()に渡される値の型
ページネーションAPIの型安全な実装#
非同期イテレータは、ページネーションAPIのラッパーに最適です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
interface Page<T> {
items: T[];
nextCursor: string | null;
}
interface PaginatedItem {
id: number;
title: string;
}
// ページネーションAPIをイテレータでラップ
async function* fetchAllPages(
baseUrl: string
): AsyncGenerator<PaginatedItem, void, undefined> {
let cursor: string | null = null;
do {
const url = cursor
? `${baseUrl}?cursor=${cursor}`
: baseUrl;
const response = await fetch(url);
const page: Page<PaginatedItem> = await response.json();
// 各アイテムを個別にyield
for (const item of page.items) {
yield item;
}
cursor = page.nextCursor;
} while (cursor !== null);
}
// 使用例: すべてのアイテムを順次処理
async function processAllItems() {
for await (const item of fetchAllPages("https://api.example.com/items")) {
console.log(`Processing: ${item.title}`);
// 各アイテムに対する処理
}
}
|
ReadableStreamとの連携#
Web Streams APIのReadableStreamも非同期イテレータとして使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
async function processStream(stream: ReadableStream<Uint8Array>) {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// valueの型: Uint8Array
console.log(`Received ${value.length} bytes`);
}
} finally {
reader.releaseLock();
}
}
// より簡潔にfor await...ofを使う場合
async function processStreamWithIterator(response: Response) {
if (!response.body) {
throw new Error("No response body");
}
// ReadableStreamをAsyncIterableとして使用
for await (const chunk of response.body) {
// chunkの型: Uint8Array
console.log(`Received ${chunk.length} bytes`);
}
}
|
TypeScript 5.6のIteratorObject型#
TypeScript 5.6では、IteratorObjectとAsyncIteratorObjectという新しい型が追加されました。これにより、イテレータに対してmapやfilterなどのメソッドを型安全に使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// TypeScript 5.6以降
async function* generateData(): AsyncGenerator<number> {
yield 1;
yield 2;
yield 3;
}
async function useIteratorHelpers() {
const iterator = generateData();
// mapメソッドを使って変換(TypeScript 5.6+)
// 注: 現時点ではAsyncIteratorのヘルパーメソッドは提案段階
// 同期イテレータではIterator.from()などが使用可能
for await (const value of iterator) {
console.log(value * 2);
}
}
|
Promiseと型ユーティリティ#
Awaited型#
TypeScript 4.5で追加されたAwaited<T>型は、Promiseを再帰的にアンラップした結果の型を取得します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// Awaited型の使用例
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<boolean | Promise<string>>; // boolean | string
// 実践的な使用例
async function processValue<T>(value: T): Promise<Awaited<T>> {
const resolved = await value;
return resolved;
}
// 使用例
const result1 = await processValue(Promise.resolve(42));
// result1の型: number
const result2 = await processValue("hello");
// result2の型: string
|
Promise関連の型ガード#
Promiseかどうかを判定する型ガードを実装できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Promiseかどうかを判定する型ガード
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
return value instanceof Promise;
}
// PromiseLikeかどうかを判定する型ガード(thenableを含む)
function isPromiseLike<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {
return (
value !== null &&
typeof value === "object" &&
"then" in value &&
typeof value.then === "function"
);
}
// 使用例
async function handleValue<T>(value: T | Promise<T>): Promise<T> {
if (isPromise(value)) {
// ここではvalueはPromise<T>
return await value;
}
// ここではvalueはT
return value;
}
|
ReturnTypeとPromise#
ReturnTypeユーティリティ型とasync関数を組み合わせる場合の注意点です。
1
2
3
4
5
6
7
8
9
10
11
|
async function fetchData() {
return { id: 1, name: "test" };
}
// async関数のReturnTypeはPromiseを含む
type FetchDataReturn = ReturnType<typeof fetchData>;
// FetchDataReturn = Promise<{ id: number; name: string }>
// Promiseの中身を取り出すにはAwaitedを組み合わせる
type FetchDataResult = Awaited<ReturnType<typeof fetchData>>;
// FetchDataResult = { id: number; name: string }
|
よくある間違いとベストプラクティス#
async関数での暗黙のany回避#
Promise<any>を避け、常に具体的な型を指定しましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// 悪い例: 戻り値がanyになる
async function badFetch(url: string) {
const response = await fetch(url);
return response.json(); // Promise<any>を返す
}
// 良い例: 明示的な型を指定
interface ApiData {
id: number;
value: string;
}
async function goodFetch(url: string): Promise<ApiData> {
const response = await fetch(url);
return response.json() as ApiData;
}
// より良い例: バリデーションを追加
import { z } from "zod";
const ApiDataSchema = z.object({
id: z.number(),
value: z.string()
});
async function bestFetch(url: string): Promise<ApiData> {
const response = await fetch(url);
const data = await response.json();
return ApiDataSchema.parse(data); // ランタイムでも型を保証
}
|
エラーハンドリングの型安全性#
catchブロックのエラーはunknown型です。適切な型ガードを使用しましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// 悪い例: error.messageに直接アクセス
async function badErrorHandling() {
try {
await fetch("https://api.example.com");
} catch (error) {
console.log(error.message); // エラー: 'error' is of type 'unknown'
}
}
// 良い例: 型ガードを使用
async function goodErrorHandling() {
try {
await fetch("https://api.example.com");
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("Unknown error:", error);
}
}
}
// より良い例: カスタムエラー型を使用
class ApiError extends Error {
constructor(
message: string,
public readonly statusCode: number
) {
super(message);
this.name = "ApiError";
}
}
async function bestErrorHandling() {
try {
const response = await fetch("https://api.example.com");
if (!response.ok) {
throw new ApiError(`HTTP ${response.status}`, response.status);
}
} catch (error) {
if (error instanceof ApiError) {
console.log(`API Error (${error.statusCode}): ${error.message}`);
} else if (error instanceof Error) {
console.log(`Error: ${error.message}`);
}
}
}
|
並列実行と逐次実行の使い分け#
パフォーマンスと型安全性を両立させましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
const userIds = [1, 2, 3, 4, 5];
// 悪い例: 逐次実行(遅い)
async function sequential() {
const users: User[] = [];
for (const id of userIds) {
const user = await fetchUser(id);
users.push(user);
}
return users;
}
// 良い例: 並列実行(速い)
async function parallel() {
const users = await Promise.all(
userIds.map(id => fetchUser(id))
);
// usersの型: User[]
return users;
}
// より良い例: エラー耐性のある並列実行
async function parallelWithErrorHandling() {
const results = await Promise.allSettled(
userIds.map(id => fetchUser(id))
);
const users = results
.filter((r): r is PromiseFulfilledResult<User> => r.status === "fulfilled")
.map(r => r.value);
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === "rejected")
.map(r => r.reason);
if (errors.length > 0) {
console.warn(`${errors.length}件のエラーが発生しました`);
}
return users;
}
|
まとめ#
本記事では、TypeScriptにおける非同期処理の型付けについて、基礎から応用まで解説しました。
重要なポイントをまとめると以下のようになります。
- Promiseの型パラメータ:
Promise<T>のTは成功時の値の型を表し、明示的に指定することで型安全性を確保できる
- async/awaitの型推論: async関数は自動的に
Promise<T>を返し、awaitはPromiseをアンラップする
- ユーティリティメソッドの型:
Promise.allはタプル型、Promise.raceはユニオン型、Promise.allSettledはPromiseSettledResult型を返す
- AbortControllerの活用: キャンセル処理を型安全に実装でき、
AbortSignal.timeoutやAbortSignal.anyで柔軟な制御が可能
- 非同期イテレータ:
AsyncGeneratorを使ってストリーミングデータやページネーションを型安全に扱える
非同期処理は現代のアプリケーション開発に不可欠です。TypeScriptの型システムを活用することで、実行時エラーを減らし、より堅牢なコードを書くことができます。
参考リンク#