非同期処理を扱っていると、「この Promise を await したら結局どんな型になるのか」を型として取り出したくなる場面があります。TypeScript には、まさにそのための組み込みユーティリティ型 Awaited<T> が用意されています。Awaited<T> は、Promise を await したあとに得られる値の型を取り出す型です。この記事では、Awaited<T> の基本的な使い方から、ネストした Promise を再帰的に剥がす挙動、ReturnType と組み合わせて async 関数の戻り値の型を取り出す実践例、そして Promise でない型を渡したときの挙動まで、具体的なコードとあわせて解説します。
目次
Awaited<T> が解決してくれること
async 関数の戻り値は必ず Promise で包まれます。たとえば Promise<string> という型の値を await すると、実際に手元に入ってくるのは string です。人間はこの「Promise を剥がした後の型」を頭の中で簡単に想像できますが、型のうえでそれを表現しようとすると意外と面倒でした。
Awaited<T> は、この「await した後に残る型」をそのまま取り出すための組み込み型です。T に Promise の型を渡すと、中身の型だけを返してくれます。まずは一番シンプルな例を見てみましょう。
// Promise<string> を await すると string になる
type A = Awaited<Promise<string>>;
// type A = string
// Promise<number> なら number
type B = Awaited<Promise<number>>;
// type B = number
// Promise で包まれていないオブジェクト型もそのまま取り出せる
type User = { id: number; name: string };
type C = Awaited<Promise<User>>;
// type C = { id: number; name: string }
Awaited<Promise<string>> が string になるのが基本の動きです。await という「値を取り出す操作」を、型の世界でそのまま再現していると考えると分かりやすいでしょう。
ネストした Promise も再帰的に剥がす
Awaited<T> の便利なところは、Promise が入れ子になっていても、いちばん奥の型まで再帰的に取り出してくれる点です。Promise<Promise<number>> のように二重・三重に包まれていても、最終的に得られる型を一発で求められます。
// 二重に包まれていても、いちばん奥の number まで取り出す type D = Awaited<Promise<Promise<number>>>; // type D = number // 三重でも同じ。再帰的にすべて剥がされる type E = Awaited<Promise<Promise<Promise<boolean>>>>; // type E = boolean
これは実際の await の挙動とも一致しています。JavaScript では Promise を await したとき、その結果がさらに Promise だった場合は自動的にもう一度待機します。つまり「Promise の中に Promise があっても、最終的にはただの値になる」わけです。Awaited<T> はこの実行時の振る舞いを型で忠実に表現しているため、入れ子の深さを気にせず中身の型を取得できます。
async 関数の戻り値の型を取り出す
実際の開発で Awaited<T> が最も活躍するのは、async 関数の「await 後の戻り値の型」を取り出したいときです。async 関数の戻り値の型を ReturnType で取り出すと、それは必ず Promise<...> で包まれた型になります。そこに Awaited を重ねると、Promise を剥がした本来のデータの型が手に入ります。
例として、API からユーザー情報を取得する非同期関数を考えます。この関数が返すデータの型を、関数の実装から自動的に導き出してみましょう。
// API からユーザーを取得する非同期関数
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return {
id: data.id as number,
name: data.name as string,
email: data.email as string,
};
}
// fetchUser の戻り値は Promise<{ id: number; name: string; email: string }>
type FetchUserReturn = ReturnType<typeof fetchUser>;
// type FetchUserReturn = Promise<{ id: number; name: string; email: string }>
// Awaited で Promise を剥がすと、await 後のデータの型が得られる
type User = Awaited<ReturnType<typeof fetchUser>>;
// type User = { id: number; name: string; email: string }
Awaited<ReturnType<typeof fetchUser>> という書き方がポイントです。内側の ReturnType<typeof fetchUser> で Promise<User> を取り出し、それを Awaited で囲むことで Promise を剥がしています。こうして得られた User 型は、関数の実装を変更すると自動的に追従します。返すオブジェクトにフィールドを足せば、この User 型にも自動でそのフィールドが反映されるため、型定義を二重にメンテナンスする必要がなくなります。
取り出した型は、別の関数の引数やコンポーネントの Props などにそのまま再利用できます。たとえば取得したユーザーを表示する関数を、この User 型を使って型付けできます。
type User = Awaited<ReturnType<typeof fetchUser>>;
// 取り出した型をそのまま引数の型として使う
function renderUser(user: User) {
console.log(`${user.name} (${user.email})`);
}
const user = await fetchUser(1);
renderUser(user); // 型が一致しているので安全に渡せる
Awaited が導入される前はどう書いていたか
Awaited<T> が組み込み型として追加されたのは TypeScript 4.5 です。それ以前は、Promise の中身を取り出す型を自分で書く必要がありました。条件型(Conditional Types)と infer を組み合わせて、次のように定義するのが定番でした。
// Awaited が無かった頃の自前実装のイメージ type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type F = UnwrapPromise<Promise<string>>; // type F = string
この自前実装でも一段の Promise なら剥がせますが、ネストした Promise を再帰的にたどったり、then を持つ独自オブジェクト(thenable)まで正しく扱ったりするには、さらに複雑な定義が必要でした。Awaited<T> はこうしたケースを標準できちんと処理してくれるため、今は自作せずに組み込み型を使うのがおすすめです。
Promise でない型を渡したときの挙動
Awaited<T> を使い始めると気になるのが、「Promise じゃない型を渡したらどうなるのか」という点です。結論から言うと、Promise で包まれていない型をそのまま渡した場合は、型は変化せずそのまま返ってきます。
// Promise で包まれていない型は、そのまま返る type G = Awaited<string>; // type G = string type H = Awaited<number[]>; // type H = number[]
これは実行時の await と同じ考え方です。Promise でない値を await しても、その値がそのまま返るだけなので、型のうえでも変化しないというわけです。この性質のおかげで、「Promise かもしれないし、そうでないかもしれない」型に対して安全に Awaited を使えます。どちらの場合でも、最終的に await して得られる型を過不足なく求められます。
then を持つ thenable も待機した後の型になる
もう一つ押さえておきたいのが、Promise クラスそのものではなくても、then メソッドを持つオブジェクト(thenable)であれば Awaited<T> が中身を取り出してくれる、という点です。JavaScript の await は Promise インスタンスに限らず、then を持つオブジェクトなら待機できる仕様になっており、Awaited<T> はその挙動も型で再現します。
// then を持つオブジェクト(thenable)
type MyThenable = {
then(onfulfilled: (value: number) => void): void;
};
// then の引数の型から、待機後の値の型 number を取り出す
type I = Awaited<MyThenable>;
// type I = number
then メソッドのコールバックが受け取る値の型が、そのまま Awaited の結果になります。標準の Promise だけでなく、ライブラリ独自の Promise 風オブジェクトを扱うときにも正しく動くのは、この thenable への対応があるおかげです。
まとめ
Awaited<T> は、Promise を await したあとに得られる型を取り出す TypeScript の組み込みユーティリティ型です。Awaited<Promise<string>> が string になるのが基本で、Promise<Promise<number>> のようにネストしていても再帰的に剥がして number を返します。実践では Awaited<ReturnType<typeof fetchUser>> のように ReturnType と組み合わせ、async 関数の戻り値の型を実装から自動で導き出す使い方が特に便利です。TypeScript 4.5 以降で使え、それ以前に必要だった自前の条件型 + infer による実装を置き換えられます。Promise でない型を渡してもそのまま返り、then を持つ thenable にも対応するため、await できるものの型を安全に求めたいときに広く使えます。