【TypeScript】非同期処理の型定義 完全ガイド|Promise・async/await・Fetch APIの型を実例で解説

TypeScriptを使ってAPI呼び出しや非同期処理を実装する際に、「Promiseの型ってどう書けばいいの?」「async関数の戻り値型は?」「fetchのレスポンスに型を付けるには?」という疑問に直面する方は非常に多いです。

非同期処理の型定義を正しく習得すると、APIレスポンスの型安全な取り扱いundefinedによるランタイムエラーの防止エディタ補完の精度向上が実現できます。本記事では、TypeScriptの非同期処理に関する型定義を基礎から実務レベルのパターンまで体系的に解説します。

この記事で学べること

  • Promise<T> の型定義基礎と関数シグネチャへの組み込み方
  • async/await 関数の戻り値型の明示と型推論の仕組み
  • Promise.all・allSettled・race・any の型推論の違い
  • Fetch API の型安全な使い方と汎用ラッパー関数の実装
  • try-catch における unknown 型のエラーハンドリング
  • Result 型パターンで型安全な非同期処理を実装する方法
  • 実務で使える APIクライアント・非同期状態の型定義パターン
  • よくある型エラーと解決方法

前提知識:TypeScriptの基本型(string, number, boolean, Union型など)を理解していることを前提とします。基礎から学びたい方は【TypeScript】型の書き方 完全入門を、関数の型については【TypeScript】関数の型定義 完全ガイドをご参照ください。

スポンサーリンク

Promise<T> の型定義基礎

Promise<T> は「将来的に型 T の値が返ってくる」ことを表す型です。T には解決値(fulfilled value)の型を指定します。

基本構文:関数の戻り値型として使う

Promise<T> の基本
// 1秒後に文字列を返す Promise
function wait(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve("完了"), 1000);
  });
}

// Promise: 数値を返す
const p1: Promise<number> = Promise.resolve(42);

// Promise: 値を返さない(副作用のみの関数)
const p2: Promise<void> = Promise.resolve();

型パラメータの省略:Promise.resolve(42) のように値から型が推論できる場合、TypeScript が自動的に Promise<number> と推論するため明示不要です。型の明示は「関数シグネチャで意図を宣言したい場合」に特に重要です。

new Promise コンストラクタの型定義

new Promise を使う場合、ジェネリクスで解決値の型を指定します。reject の引数型は常に any です。

new Promise の型定義
function readFile(path: string): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    // resolve の引数は string 型のみ受け付ける
    resolve("ファイル内容");        // OK
    // resolve(42);               // NG: number は string に代入不可
    reject(new Error("読み込み失敗"));  // reject は any を受け付ける
  });
}

注意:Promise の型パラメータは resolve の値のみに型チェックが働きます。reject に渡す値の型は any であり、TypeScript は拒否理由の型を追跡しません。エラー型を型安全に扱うには後述の「Result 型パターン」が有効です。

async/await 関数の型定義

async キーワードを付けた関数は、返り値が自動的に Promise<T> にラップされます。

async 関数の戻り値型

async 関数の戻り値型(型推論 vs 明示)
// 型推論:TypeScript が Promise と推論
async function getNumber() {
  return 42;  // 推論: Promise
}

// 明示的な戻り値型(公開 API・複雑なロジックに推奨)
async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as User;
}

// Promise:値を返さない async 関数
async function sendLog(msg: string): Promise<void> {
  await postToServer(msg);
  // return しない → void
}

await の型アンラップ

awaitPromise<T>T に変換します。この型アンラップは TypeScript も追跡するため、await した変数は自動的に正しい型になります。

await のアンラップ(Promise<T> → T)
async function example() {
  const p: Promise<number> = Promise.resolve(42);
  const n: number = await p;  // Promise → number

  // ネストした Promise も1段ずつアンラップ
  const nested = Promise.resolve(Promise.resolve("hi"));
  const s = await nested;  // 型: string(Promise は自動フラット化)
}

Awaited<T> ユーティリティ型(TypeScript 4.5+)

TypeScript 4.5 で追加された Awaited<T> は、Promise を再帰的にアンラップした型を取得するユーティリティ型です。Awaited<Promise<string>>string になります。

Awaited<T> の使い方
// Awaited: Promise を再帰的にアンラップ
type A = Awaited<Promise<string>>;          // type A = string
type B = Awaited<Promise<Promise<number>>>;  // type B = number (再帰的にアンラップ)

// ReturnType と組み合わせて非同期関数の解決型を取得
async function fetchUser(id: number) {
  return { id, name: Alice, age: 30 };
}

// fetchUser の解決値の型を取得(型定義の再利用に便利)
type UserType = Awaited<ReturnType<typeof fetchUser>>;
// type UserType = { id: number; name: string; age: number }

// Promise.all の戻り値型取得にも活用
type AllResult = Awaited<ReturnType<typeof loadAll>>;

Awaited<T> の実務活用:async 関数の戻り値型を別の場所で再利用したい場合、Awaited<ReturnType<typeof functionName>> のパターンで型定義の重複を避けられます。インターフェースを別途定義しなくても、関数自体から型を抽出できる強力なテクニックです。

Promise.all・allSettled・race・any の型定義

複数の Promise を並列実行する際に使うコンビネータは、それぞれ異なる型推論の挙動を持ちます。使い分けを正しく理解しておきましょう。

Promise.all のタプル型推論

Promise.all は渡した配列に対応するタプル型を返します(TypeScript 3.1 以降)。

Promise.all の型推論
async function loadAll() {
  const [user, posts, count] = await Promise.all([
    fetchUser(1),       // Promise
    fetchPosts(1),      // Promise
    fetchPostCount(1),  // Promise
  ]);
  // 推論: [User, Post[], number]
  console.log(user.name, posts.length, count);
}

Promise.allSettled の型定義

Promise.allSettled は成功・失敗問わず PromiseSettledResult<T>[] を返します。status プロパティで型を絞り込めます。

Promise.allSettled の型
async function loadWithFallback() {
  const results = await Promise.allSettled([
    fetchUser(1),  // Promise
    fetchUser(2),  // Promise
  ]);
  // 型: PromiseSettledResult[]

  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log(result.value.name);  // User 型
    } else {
      console.error(result.reason);      // 失敗理由
    }
  });
}

Promise.race と Promise.any

Promise.race / Promise.any の型
// Promise.race: 最初に解決/拒否した Promise の型を返す
const fast: string = await Promise.race([
  fetchFast(),  // Promise
  fetchSlow(),  // Promise
]);

// Promise.any: 最初に成功した Promise の型を返す(全部失敗→ AggregateError)
const result: Data = await Promise.any([
  tryPrimary(),   // Promise
  tryFallback(),  // Promise
]);
メソッド 戻り値型 一つでも拒否すると
Promise.all [T1, T2, ...] 即座に reject
Promise.allSettled PromiseSettledResult<T>[] 全て完了まで待つ
Promise.race T(最初に確定した値) 最初の結果が拒否なら reject
Promise.any T(最初に成功した値) 全部失敗なら AggregateError

Fetch API の型定義

fetch() の戻り値型は Promise<Response> です。しかし Response.json() の戻り値は Promise<any> のため、型アサーションかジェネリクスラッパーで型を付ける必要があります。

基本的な fetch の型付け

fetch の基本的な型定義
interface Post {
  id: number;
  title: string;
  body: string;
}

async function getPost(id: number): Promise<Post> {
  const response: Response = await fetch(`/api/posts/${id}`);

  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`);
  }

  // Response.json() は Promise。型アサーションで型を付ける
  const data = await response.json() as Post;
  return data;
}

型安全な汎用 fetch ラッパー(推奨パターン)

毎回型アサーションを書くのは冗長です。ジェネリクスを使った汎用ラッパーを作ることで、型安全かつ再利用可能な fetch 関数が実装できます。

汎用 fetch ラッパー関数
async function apiFetch<T>(
  url: string,
  options?: RequestInit
): Promise<T> {
  const response = await fetch(url, options);

  if (!response.ok) {
    const msg = await response.text();
    throw new Error(`API Error ${response.status}: ${msg}`);
  }

  return response.json() as Promise<T>;
}

// 使用例:型引数で返り値の型を明示
const post  = await apiFetch<Post>("/api/posts/1");
const users = await apiFetch<User[]>("/api/users");
// post は Post 型、users は User[] 型として推論される

AbortController との組み合わせ(タイムアウト処理)

タイムアウト付き fetch
async function fetchWithTimeout<T>(
  url: string,
  timeoutMs: number = 5000
): Promise<T> {
  const controller = new AbortController();
  const timer = setTimeout(
    () => controller.abort(),
    timeoutMs
  );
  try {
    return await apiFetch<T>(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

型アサーション(as T)の制限:上記のラッパーは as Promise<T> でキャストしますが、実際のレスポンス構造を TypeScript は検証しません。型が一致しないデータが返ってきた場合も型エラーにならないため、本番環境では Zod や yup などのバリデーションライブラリを併用することを推奨します。

axios を使う場合の型定義

axios では axios.get<T>(url) のようにジェネリクスを指定でき、response.data が型 T になります。fetch より直感的に型を付けられます。

axios の型定義
// axios.get で response.data の型を指定
import axios from axios;

async function getUser(id: number): Promise<User> {
  const response = await axios.get<User>();
  return response.data;  // User 型として推論される
}

// axios のエラー型: AxiosError を使って型ガード
import { axios, AxiosError } from axios;

try {
  await getUser(1);
} catch (err) {
  if (axios.isAxiosError(err)) {
    console.log(err.response?.status);  // number | undefined
    console.log(err.message);         // string
  }
}

try-catch のエラー型定義

TypeScript 4.0 以降、catch 節の変数は unknownになりました(useUnknownInCatchVariables: true がデフォルト)。そのため、エラー変数を使う前に型ガードが必要です。

catch 節の型ガード
async function safeFetch(url: string) {
  try {
    return await apiFetch<unknown>(url);
  } catch (err) {
    // TypeScript 4.0+: err は unknown 型
    if (err instanceof Error) {
      console.error(err.message);  // OK: string として推論
    } else {
      console.error("予期しないエラー:", err);
    }
  }
}

カスタムエラークラスの型定義

カスタムエラークラス
class ApiError extends Error {
  constructor(
    public readonly statusCode: number,
    public readonly endpoint: string,
    message: string
  ) {
    super(message);
    this.name = "ApiError";
  }
}

async function callApi(url: string) {
  const res = await fetch(url);
  if (!res.ok) {
    throw new ApiError(res.status, url, await res.text());
  }
  return res.json();
}

// 使用時: instanceof で ApiError を絞り込む
try {
  await callApi("/api/data");
} catch (err) {
  if (err instanceof ApiError) {
    console.log(err.statusCode);  // number
    console.log(err.endpoint);   // string
  }
}

Result 型パターン(例外を使わない型安全なエラーハンドリング)

例外の代わりに、成功か失敗かを型で表現する Result 型パターンを使うと、呼び出し元でのエラーハンドリングを型として強制できます。

Result 型パターン
// Result 型の定義
type Ok<T> = { ok: true;  value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;

// ヘルパー関数
const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
const err = <E>(error: E): Err<E> => ({ ok: false, error });

// Result 型を返す非同期関数
async function safeGetUser(id: number): Promise<Result<User>> {
  try {
    const user = await apiFetch<User>(`/api/users/${id}`);
    return ok(user);
  } catch (e) {
    return err(e instanceof Error ? e : new Error(String(e)));
  }
}

// 呼び出し元: ok フラグで型を絞り込む
const result = await safeGetUser(1);
if (result.ok) {
  console.log(result.value.name);   // User 型
} else {
  console.error(result.error.message);
}

エラーに関する詳細なトラブルシューティングは【TypeScript】よくあるエラーと解決方法もあわせて参照してください。

実務で使える型定義パターン

API レスポンス共通型

実務の API では共通のレスポンス構造を持つことが多いです。ジェネリクスを使った共通型を定義することで、全 API コールに一貫した型安全性を適用できます。

API レスポンス共通型
// 単一リソース
interface ApiResponse<T> {
  data:    T;
  message: string;
  success: boolean;
}

// ページネーション
interface Paginated<T> {
  data:     T[];
  total:    number;
  page:     number;
  pageSize: number;
}

// 使用例
async function getUsers(page: number): Promise<Paginated<User>> {
  return apiFetch<Paginated<User>>(`/api/users?page=${page}`);
}

非同期状態の型定義(React/UI フレームワーク向け)

「idle・loading・success・error」の 4 状態を型で表現すると、状態管理のバグを型レベルで防げます。Discriminated Union を活用したパターンです。

非同期状態の型定義(Discriminated Union)
type AsyncState<T> =
  | { status: "idle"                          }
  | { status: "loading"                       }
  | { status: "success"; data: T          }
  | { status: "error";   error: Error      };

// 使用例(React useState)
const [state, setState] = useState<AsyncState<User>>({ status: "idle" });

async function loadUser(id: number) {
  setState({ status: "loading" });
  try {
    const user = await apiFetch<User>(`/api/users/${id}`);
    setState({ status: "success", data: user });
  } catch (e) {
    const error = e instanceof Error ? e : new Error(String(e));
    setState({ status: "error", error });
  }
}

// render 時: status で data の存在が型レベルで保証される
if (state.status === "success") {
  console.log(state.data.name);  // OK: User 型
}

React + TypeScript での型定義全般については【TypeScript】React + TypeScriptの始め方も参照してください。

よくあるエラーと解決方法

エラーメッセージ 原因 解決方法
Property 'data' does not exist on type 'Response' Response 型を直接使用している response.json() as T で型変換
Object is of type 'unknown' catch 節の変数を型ガードなしで使用 instanceof Error で型を絞り込む
Type 'Promise<T>' is not assignable to type 'T' async 関数の戻り値を await せずに使っている await を追加する
async function は Promise<T> を返すのに戻り値型が T async 関数の戻り値型を非 Promise 型に指定 戻り値型を Promise<T> に修正
'await' expressions are only allowed within async functions async でない関数内で await を使用 関数に async キーワードを追加
よくあるエラーの修正例
// ❌ NG: catch 節の err を直接使う
try { /* ... */ } catch (err) {
  console.log(err.message);  // Error: Object is of type 'unknown'
}

// ✅ OK: instanceof で型ガード
try { /* ... */ } catch (err) {
  if (err instanceof Error) {
    console.log(err.message);  // OK: string
  }
}

// ❌ NG: async 関数の戻り値を await なしで使う
const user: User = getUser(1);  // Error: Promise は User に代入不可

// ✅ OK: await する
const user: User = await getUser(1);

まとめ

TypeScript の非同期処理における型定義の要点をまとめます。

TypeScript 非同期処理 型定義まとめ

  • Promise<T> は「将来的に T を返す」型。async 関数は自動的に Promise<T> を返す
  • await は Promise<T> を T にアンラップ。TypeScript も型を追跡する
  • Promise.all はタプル型を返す。allSettled は PromiseSettledResult<T>[] を返す
  • fetch の Response.json() は any 型。型アサーションか汎用ラッパーで型を付ける
  • catch 節の変数は unknown 型(TS 4.0+)。instanceof Error で型ガードが必要
  • Result 型パターン で例外を使わない型安全なエラーハンドリングが可能
  • AsyncState<T>(idle/loading/success/error)で UI の状態管理を型安全に

非同期処理の型定義をマスターすると、API を扱うコードの安全性が大幅に向上します。次のステップとしてジェネリクスの深い理解ユーティリティ型(Partial・Pick・Omit 等)も合わせて学ぶと、より型安全なコードが書けるようになります。

よくある質問(FAQ)

async 関数の戻り値型は毎回明示すべきですか?

公開 API や複雑なロジックを持つ関数には明示を推奨します。戻り値型を明示することで、「意図した型と実装のズレ」をコンパイル時に検出できます。シンプルな内部関数は型推論に任せても問題ありません。

Promise<void> と Promise<undefined> の違いは何ですか?

Promise<void> は「値を返すことを意図しない」ことを表します。Promise<undefined> は「明示的に undefined を返す」意味合いです。副作用のみを持つ関数(ログ送信・状態更新など)には Promise<void> を使うのが慣例です。

axios を使う場合も型定義方法は同じですか?

axios は axios.get<T>(url) のようにジェネリクスで戻り値型を指定でき、response.data が型 T になります。fetch よりも直感的に型を付けられます。ただし axios の型定義は response.data のみに適用されるため、エラーハンドリング(AxiosError の型)には同様の配慮が必要です。

Promise チェーン(.then/.catch)にも型は付きますか?

はい。.then((value: T) => ...) の形でコールバックの引数に型が付き、TypeScript はチェーンを追跡します。ただし async/await の方が型推論が分かりやすく補完も強力なため、特別な理由がない限り async/await を推奨します。