はじめに

JavaScriptの非同期処理は、コールバック地獄からPromise、そしてasync/awaitへと進化してきました。TypeScriptを使えば、この非同期処理に型安全性を加えることができます。しかし、Promiseの型定義やasync関数の戻り値の型推論、複数のPromiseを組み合わせたときの型の扱いなど、理解すべきポイントは少なくありません。

本記事では、TypeScriptにおける非同期処理の型付けを体系的に解説します。Promiseの型パラメータの意味から、async/awaitでの型推論の仕組み、Promise.allPromise.raceなどのユーティリティメソッドの型、さらにAbortControllerを使ったキャンセル処理や非同期イテレータまで、実践的な内容をカバーします。

この記事を読み終えると、以下のことができるようになります。

  • Promise<T>の型パラメータを正しく理解し、適切に型定義できる
  • async/await構文での型推論の仕組みを理解し、活用できる
  • Promise.allPromise.racePromise.allSettledの型を正確に扱える
  • AbortControllerを使った型安全なキャンセル処理を実装できる
  • 非同期イテレータ(for await...of)を型安全に使用できる

実行環境・前提条件

前提知識

動作確認環境

ツール バージョン
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>;
}

この型定義から、以下のことがわかります。

  1. Tは成功時の値の型: Promise<string>なら、成功時にstring型の値が得られる
  2. thenのチェーンで型が変わる: thenに渡す関数の戻り値によって、次のPromiseの型が決まる
  3. エラーの型はany: catchonrejectedで受け取るエラーの型はany(厳密にはunknownが推奨)
flowchart LR
    A["Promise&lt;T&gt;"] --> 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では、AbortControllerAbortSignalの型が組み込みで定義されています。

 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では、AsyncIteratorAsyncIterableの型が定義されています。

 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では、IteratorObjectAsyncIteratorObjectという新しい型が追加されました。これにより、イテレータに対してmapfilterなどのメソッドを型安全に使用できます。

 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における非同期処理の型付けについて、基礎から応用まで解説しました。

重要なポイントをまとめると以下のようになります。

  1. Promiseの型パラメータ: Promise<T>Tは成功時の値の型を表し、明示的に指定することで型安全性を確保できる
  2. async/awaitの型推論: async関数は自動的にPromise<T>を返し、awaitはPromiseをアンラップする
  3. ユーティリティメソッドの型: Promise.allはタプル型、Promise.raceはユニオン型、Promise.allSettledPromiseSettledResult型を返す
  4. AbortControllerの活用: キャンセル処理を型安全に実装でき、AbortSignal.timeoutAbortSignal.anyで柔軟な制御が可能
  5. 非同期イテレータ: AsyncGeneratorを使ってストリーミングデータやページネーションを型安全に扱える

非同期処理は現代のアプリケーション開発に不可欠です。TypeScriptの型システムを活用することで、実行時エラーを減らし、より堅牢なコードを書くことができます。

参考リンク