🎄

Awaited<T> / TypeScript一人カレンダー

2022/12/13に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の2日目です。昨日は『ReturnType<T>』を紹介しました。

Awaited<T>

2日目に紹介するのはAwaited<T>です。

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

Awaited<T>はTypeScript 4.5と、比較的最近追加されたものでありながら、とても便利なUtility Typesのひとつです。

使い方は簡単なので公式ドキュメントの例を確認しましょう。

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;

このようにAwaited<T>の型パラメータにPromise<T>があれば、そのTだけを得ることができます。ここで特徴的なのは、type BのようにPromise<T>がネストしていてもすべて解決してくれるという点です。そしてtype Cを見れば分かる通り、Promise<T>ではないboolean型が混ざっている場合でも、それはエラーとなりません。すなわちAwaited<T>からはTを得られます。

どんなときに便利?

Awaited<T>を使う状況を2つ紹介します。

例1

例えば、Electronアプリケーションにて、SQLiteから情報を取得して画面上に描画したいケース。なんらかの記事を全件取得するgetEntries()という関数を実装する状況で考えてみましょう。接続にはnode-sqliteを使っている想定ですが、ここでは雰囲気でよいのでSQLiteがなんなのかなどは気にしなくてよいです。

const sqliteDb = /* sqlite のコネクションオブジェクト */
const query = /* Entry を全件取得するSQLiteクエリ */

async function getEntries(): Promise<Entry[]> {
  const result = await sqliteDb.all(query);
  return adaptEntries(result);
}

SQL系のクエリを実行して情報を取得すると、JOINを用いる場合にネストしたオブジェクトとして出力されずに配列として出力されることが一般的です。たとえば記事データEntryにタグが2つ付いていたとすると、理想と現実が異なる事がわかります。

// 理想
const entry = {
  id: "entryId1",
  title: "記事名",
  text: "本文",
  tags: [
    { id: "tagId1", name: "タグ1" },
    { id: "tagId2", name: "タグ2" },
  ],
};
// 現実
const entry = [
  {
    entryId: "entryId1",
    entryTitle: "記事名",
    entryText: "本文",
    tagId: "tagId1",
    tagName: "タグ1",
  },
  {
    entryId: "entryId1",
    entryTitle: "記事名",
    entryText: "本文",
    tagId: "tagId2",
    tagName: "タグ2",
  },
];

1つのentryを取り出そうとしてもタグの数だけ配列として返ってきてしまうことがわかります。そのため、こういった状況ではO/Rマッパーの採用を検討することもできますが、筆者の場合は個別にadapt関数を実装するようにしています。

そんなときにTypeScriptで面倒なのが、JOINする場合、しない場合ごとに型を毎回定義しないといけないように感じる点。筆者はこの状況をAwaited<T>と昨日紹介のReturnType<T>で克服しています。

async function getEntries(): Promise<ReturnType<typeof adaptEntries>> {
  const result = await sqliteDb.all(query);
  return adaptEntries(result);
}

getEntries()の戻り型をPromise<ReturnType<typeof adaptEntries>>としました。Promise<T>でラップしているのは、この関数がasync functionであるためです。ただしこのままアプリケーション内の至るところでReturnType<typeof adaptEntries>という表記で記述してしまうのは、データベース変換に関する内部の詳細な実装が露出することにつながり避けたいです。そういった場合に、次のように型定義することができます。

type Entries = Awaited<ReturnType<typeof getEntries>>`;

このgetEntries()関数を扱う箇所ではこの型が得られる、ということが示されています。

普通に教科書的にtype Entry = { /* ... */ }と書いてももちろん問題はないのですが、だいたいこういったアプリケーションは「メイン表示用のEntry」、「サイドバー用のEntry」、「検索時の予測表示のためのEntry」のように「似てるけど微妙に違うEntry」が大量に出てきてしまうものです。そのため、ReturnType<typeof getEntries>を使って「この関数はメイン表示用途のクエリとその変換実装を備えている」、「この関数はサイドバー表示用途のクエリとその変換実装を備えている」といった風に用途ごとの関数を定義してしまい、その関数を正として型のつないでいくこともできます。

こうすることで、adaptEntries()関数を実装するファイルでは関数の近辺に大元となるtype Entry =が記述されるわけですが、その型はexportせずに関数のみexportすればよくなります。多くの場合、関数と型の両方にexportを付与し、毎回2つimportしているような状況をよくみますが、そうしないことも可能なのです。そしてこのアプローチを試みる場合、Promise<T>が付いているか否かを気にする必要が従来のTypeScriptバージョンまではありました。今ではAwaited<T>の採用のおかげでこういったデータベース絡みの処理だとしても、実装内部の型を参照できるようになりました。

ただしこのアプローチは万能ではなく、バックエンドとフロントエンドを明確に区別して実装したい場合などはかえって依存関係が複雑になる(データベースの型がテンプレート層まで伝播してしまう)という懸念があります。今回の例はあくまでもElectron + SQLite + Reactという想定で挙げているため、こういったデータベースから描画までを一貫するというアプローチも可能である、という程度でお伝えしています。

例2

もう一つみてみましょう。続いての例は昨日も取り上げたFirestoreに関するものです。

Firestoreではfirestore.collection('name').doc('id').get()のようにドットチェーン形式で情報を取得するAPIがかつて存在しました。現在ver. 9系ではこうではなくなっているので、今回は一種の負債取り扱いのネタとして取り上げています。このとき、get()の戻り値の型はなんなのか?と気になったら、多くの状況ではエディタを使って型定義ファイルへジャンプするでしょう。そしてそこにPromise<DocumentSnapshot<T>>と書かれていたのであればそれをコピペして使う。大概の場合はこの流れだと思います。

別の面白いアプローチとして、次のように書くこともできます。

import { firestore } from './firestore'; // 別のファイルで Firestore の初期化を済ませてある

type Snapshot = Awaited<
  ReturnType<ReturnType<ReturnType<typeof firestore.collection>['doc']>['get']>
>;

長くて複雑そうですが、ひとつずつ見ていきます。

type CollectionReturn = ReturnType<typeof firestore.collection>;
type DocReturn = ReturnType<CollectionReturn['doc']>;
type GetReturn = ReturnType<DocReturn['get']>;
type Snapshot = Awaited<GetReturn>;

このようになっています。ReturnType<T>がネストできるだけでなく、ReturnType<CollectionReturn['doc']>のように['doc']としてプロパティ名を記述できることは知らない方もいるかもしれません。こうすることでReturnType<T>を使いながらドットチェーンを辿っていくような型も記述することができます。最後はPromise<T>型だったため、Awaited<T>を使ってPromiseを外せば完成。

この状況ならFirestoreの型定義ファイルからDocumentSnapshot<T>をimportしたほうが短い上に確実であるため、わざわざReturnType<T>を使って書く必要はありません。技術的な調査をするときや、実装経緯を追いたい際にどういった型定義を参照しているのか確認したいときなど、こういったReturnType<T>を使った型を一時的にメモ代わりに書いておくことで調査が捗ったりします。大手の型定義ファイルはだいたい長すぎてジャンプしても見失ってしまうことがありますから、筆者は構造整理にこの手法をよく使います。

明日は『実例 UnArray<T>』

昨日のReturnType<T>、本日のAwaited<T>は実装や型定義ファイルから内部の別の型を参照したい場合に有用であることがわかりました。Awaited<T>は単独だといつ使うかわからないかもしれませんが、このように組み合わせていくと活用の幅が広がります。

明日Awaited<T>の内部がどうなっているかと、その仕組を活用したお手製Utility Types UnArray<T>を紹介します。それではまた。

Discussion