TypeScriptを使ってAPI呼び出しや非同期処理を実装する際に、「Promiseの型ってどう書けばいいの?」「async関数の戻り値型は?」「fetchのレスポンスに型を付けるには?」という疑問に直面する方は非常に多いです。
非同期処理の型定義を正しく習得すると、APIレスポンスの型安全な取り扱い・undefinedによるランタイムエラーの防止・エディタ補完の精度向上が実現できます。本記事では、TypeScriptの非同期処理に関する型定義を基礎から実務レベルのパターンまで体系的に解説します。
この記事で学べること
- Promise<T> の型定義基礎と関数シグネチャへの組み込み方
- async/await 関数の戻り値型の明示と型推論の仕組み
- Promise.all・allSettled・race・any の型推論の違い
- Fetch API の型安全な使い方と汎用ラッパー関数の実装
- try-catch における
unknown 型のエラーハンドリング
- Result 型パターンで型安全な非同期処理を実装する方法
- 実務で使える APIクライアント・非同期状態の型定義パターン
- よくある型エラーと解決方法
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 の型アンラップ
await は Promise<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 を推奨します。