🐕

[TypeScript] Awaited<T>を知っていますか?

2024/08/20に公開

先日チームメンバーのコードレビューをしていた際、Awaitedを使ったコードを見つけました。
個人的にはなんとなく最近追加されたTypeScriptの型ユーティリティだな〜くらいの認識で実務で使ったことがなかったので、せっかくの機会なので調べてみました。

ユーティリティ Awaited<T>型とは?

Awaited は、TypeScript 4.5 で導入された型ユーティリティです。Promise の結果を型として抽出するのに役立ちます。つまり、Promise が解決されたときに返される値の型を取得するために使用できます。
この型は再帰的に定義されており、Promise が多重にネストされている場合でも、最終的な解決された値の型を抽出できます。

TypeScript Documentationのサンプル

以下は、 TypeScript Documentationのサンプルコードの内容です

type A = Awaited<Promise<string>>;
=> type A = string

type B = Awaited<Promise<Promise<number>>>;
=> type B = number
 
type C = Awaited<boolean | Promise<number>>;
=> type C = number | boolean

https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype

具体的な使用例

以下は、 Awaited 型の具体的な使用例です。

async function fetchHoge() {
  return "Hello, World!";
}

type Result = Awaited<ReturnType<typeof fetchHoge>>;
// Resultの型は `string` になります。

この例では、fetchHohe 関数は Promise<string> を返しますが、Awaited 型を使用することで、その結果である string 型を得ることができます。

せっかくなので内部実装を見てみる

Awaitedがどのように型定義されているかをせっかくなので詳しく見てみます。

https://github.com/microsoft/TypeScript/blob/2a8865e6ba95c9bdcdb9e2c9c08f10c5f5c75391/src/lib/es5.d.ts#L1542-L1550

type Awaited<T> = 
  T extends null | undefined    // ① T が null または undefined であれば、そのまま T を返す。
  ? T
  : T extends object & { then(onfulfilled: infer F, ...args: infer A): any }  // ② T が then メソッドを持つオブジェクト(Promise など)であるかを確認。
  ? F extends (value: infer V, ...args: A) => any    // ③ then メソッドの onfulfilled コールバックの型 F が、ある値 V を引数に取るか確認。
    ? Awaited<V>   // ④ V に対して再帰的に Awaited を適用し、最終的な型を得る。
    : never  // ⑤ もし V が推論できない場合、never 型を返す。
  : T;   // ⑥ T が Promise でも then メソッドを持つオブジェクトでもない場合、そのまま T を返す。

次のようなプロセスで Awaited 型を決定しているようです

  1. null または undefined のチェック:
    T に null や undefined が与えられた場合はそれらをそのまま型として返しています。
  2. Promise かどうかのチェック:
    T がオブジェクトであり、かつ then メソッドを持っているかどうかを確認し Promise のような構造をしているかを確認しています
  3. then メソッドの型をチェック:
    then メソッドの最初の引数が関数であり、その関数が特定の値(V)を引数に取るかを確認しています
  4. 再帰的な適用:
    上記で推論されたVに対して再帰的にAwaited型を適用し、最終的に解決された型を返します。これによってPromise<Promise<number>> のような多重ネストされた場合でも最終的な解決済みの型を取得できます。
  5. 推論できない場合
    もし V の型を推論できない場合はneverとしています。コールバックの型が適切でない場合のエラーハンドリング的な意味合いに見えます。
  6. 最終的な型の決定:
    最後に、T が Promise でない場合はそのまま T を返しています。

あまり自分で型パズルのようなものを作ることがないので非常に勉強になりましたし、Awaitedについても理解を深めることができました。

実務的な使用例を考えてみる

実務では前もってPromiseの返り値の型定義は事前にされていることが多いので、あまりAwaitedの出番はないと感じています。どういった場合に使いどきがありそうかいくつか考えてみました。

例1) APIコールが行われている関数の返り値の推論

API コールがある場合の関数などで、そのような関数の結果を引数としてとるような関数を使う場合にAwaitedはうまく活用できそうですし、普段の利用イメージも持てました。

async function fetchUser(): Promise<{ id: number; name: string }> {
  return { id: 1, name: "Funteractive User" };
}

async function fetchHoge(userId: number): Promise<{ fuga: number; }[]> {
  return [{ fuga: 101 }];
}

async function getUserData() {
  const user = await fetchUser();
  const hoge = await fetchHoge(user.id);

  return { user, hoge };
}

// userData の型は { user: { id: number; name: string }, hoge: { fuga: number } } になります
async function doSomethingUser(userData: Awaited<ReturnType<typeof getUserData>>) {
    return "do something by Userdata";
}

例2)Reactで非同期操作を扱うhooksの作成

カスタムフックが非同期データを返す場合、Awaited を使用してフックの返り値の型を安全に定義することがでそうです。
例えば小さなアプリケーションでReactQueryやSWRなどのライブラリをいれるまでもない場合などは、以下のように型安全にデータfetchを扱うユーティリティhooksを作るといい感じに使えそうです。

function useFetchData<T>(fetchFunction: () => Promise<T>): { data: Awaited<T> | null; error: Error | null; isLoading: boolean } {
  const [data, setData] = React.useState<Awaited<T> | null>(null);
  const [error, setError] = React.useState<Error | null>(null);
  const [isLoading, setIsLoading] = React.useState<boolean>(false);

  React.useEffect(() => {
    setIsLoading(true);
    fetchFunction()
      .then(result => {
        setData(result);
        setError(null);
      })
      .catch(err => {
        setError(err instanceof Error ? err : new Error(String(err)));
        setData(null);
      })
      .finally(() => {
        setIsLoading(false);
      });
  }, [fetchFunction]);

  return { data, error, isLoading };
}

// 使用例
async function fetchHoge(): Promise<{ id: number; name: string; }> {
  return { id: 1, name: "AAA" };
}

const { data: hoge, error, isLoading } = useFetchData(fetchHoge);
// hoge の型は { id: number; name: string; } | null になります

if (isLoading) {
  console.log("Loading...");
} else if (error) {
  console.error("Failed:", error);
} else if (hoge) {
  console.log("fetched:", hoge);
}

例3) 型安全なデータ取得のハンドリング

非同期関数の実行結果をエラーハンドリングしやすい形式にまとめるためのユーティリティ関数を作ってみます。safeFetch関数は、非同期処理の成功時と失敗時の両方に対応し、エラーが発生しても例外をスローせずに結果を一つのオブジェクトにまとめて返すため、使い勝手が良くなりそうです。

type Result<T> = { data: Awaited<T> | null; error: Error | null };

async function safeFetch<T>(asyncFunction: () => Promise<T>): Promise<Result<T>> {
  try {
    const data = await asyncFunction();
    return { data, error: null };
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    return { data: null, error: err };
  }
}

// 使用例
async function getHogeData(): Promise<Result<{ id: number; name: string; price: number }>> {
  return await safeFetch(fetchHoge);
}
// getHogeData の型は { data: { id: number; name: string; } | null; error: Error | null } になります

まだまだありそうなので、もし思いついたものなどがあれば教えていただければ幸いです。

余談

今回はAwaitedってなんだっけみたいな些細な疑問から、内部実装理解・活用イメージまで広げることができてよかったです。

弊社のメンバーにも雑に聞いてみたところ、僕と同じく使ったことないメンバーやそもそも知らなかったメンバーもいるようだったので知見にもなってよかったです。

ファンタラクティブテックブログ

Discussion