🎄

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

2022/12/12に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の1日目です。

Utility Types

TypeScriptにはUtility Typesというものがあります。TypeScript 2.1から徐々に導入され、4.9の現在では、Utility Typesを全く知らないままTypeScript開発を進めるのはもったいないと言えるほど充実しました。

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

本記事や一連のアドベントカレンダーでは、そんなUtility Typesを中心にTypeScriptの活用について紹介していきます。そのまま説明するだけではドキュメントの日本語訳になってしまいますので、たとえ文量が短かったとしても筆者の経験や実例を中心に紹介する方向で予定しています。

ReturnType<T>

1日目を飾るのはReturnType<T>型です。この型では、型パラメータTに与えた関数の戻り型を得ることができます。

例えば、引数がなく文字列を返す関数の型() => stringがあるとします。この型にFunctionReturnStringという別名を付けました。そしてReturnType<FunctionReturnString>型をResult型として定義すると、Result型は stringとなっています。

type FunctionReturnString = () => string;
type Result = ReturnType<FunctionReturnString>;
//   ^? string

別の例を試してみましょう。 FunctionReturnNumber 型を定義してみます。

type FunctionReturnNumber = () => number;
type Result = ReturnType<FunctionReturnNumber>;
//   ^? number

Result型はnumberとなりました。このように簡単に戻り型のみを抽出できます。

どんなときに便利?

この例だけだと業務で使う場面が思いつかないかもしれません。そこで筆者の経験による使い方を紹介します。

まず、この型はTypeof Type Operator typeofと組み合わせるのが非常に有用です。

例1

例えば、ReactとFirestoreを組み合わせる案件で react-firebase-hooksを使うとします。こういった状況だと、依存には複数の型定義が登場してしまいます。

import { getFirestore } from "@firebase/firestore";
import { captureException } from "@sentry/hub";
import { getApp } from "firebase/app";
import { useDocument } from "react-firebase-hooks/firestore";

このような状況でライブラリが提供する型を使いたいとき、ひとつずつ型定義ファイルを調べていってその内容をコピペしてもよいですが、コピペ元の定義が変更されてしまった場合に追従できずエラーとなってしまいます。そこで追従できるようにこういった書き方をします。次のlet dbを確認してください。

let db: ReturnType<typeof getFirestore>;
try {
  db = getFirestore(getApp("app-name"));
} catch (e) {
  if (e instanceof Error) {
    // Sentry にエラーアラートを送信
    // 実際の現場でのエラーメッセージは例とは異なる
    captureException(new Error("firestore取得異常"));
  }
  throw e;
}

ここで、letではなくconst db = getFirestore()としてオブジェクトを得るようであれば型推論が有効になるので問題ありません。ところが、ここでtry-catchを記述するとなると話が変わってきます。tryブロック内で変数を格納したいためにlet dbという宣言を事前にすることになり、そこでdbの型を明示的に書く必要があるのです。

普段なら型推論に頼れるところを明記しなければならない。こういった場面で型定義ファイルをコピペしてしまう人は多いですが、ここがまさにReturnType<T>の出番です。

typeof getFirestoreを使うことで、getFirestore()関数のシグネチャの型をそのまま得ることができますので、その結果をReturnType<T>に渡すだけでgetFirestore()が返却する値の型だけを名指しすることができます。

let db: ReturnType<typeof getFirestore>;
// 型定義ファイル から getFirestore() の記述をコピペしてこなくてよい

例2

Firestore関連で別の使い方をもうひとつ紹介します。

const [value, isLoading, error] = useDocument(doc(db, "collection", id));

useDocument() Hook は、このようにオブジェクトではなく length 3 の Tuple を返します。[0]value, [1]isLoading, [2]errorという変数名であるかどうかは任意なため、ここで名付け間違ったとしてもコンパイラ上では誤りとなりません。単に開発者自身が名付けを注意せねばならないという状況です。

こんなときは名前が付くようにオブジェクトにしてしまいましょう。

type Return = {
  readonly value: ReturnType<typeof useDocument>[0];
  readonly isLoading: ReturnType<typeof useDocument>[1];
  readonly error: ReturnType<typeof useDocument>[2];
};

ReturnType<typeof useDocument>[A, B, C]という形式のTupleを返却します。そのためReturnType<typeof useDocument>[0]のように取り出すことができるのです。この場面では[0]は実際にはDocumentSnapshot<unknown> | undefinedを取り出しています。

注意点

このように使い方を覚えると活用の幅が広がるReturnType<T>ですが、注意点もあります。それは次の例です。

type T2 = ReturnType<<T>() => T>;
//   ^? unknown

ドキュメントでも紹介されているこの例では、T 型を返すようになっています。残念ながら TypeScript 4.9 では ReturnType<T><U> といった表記ができないため、この箇所は unknown として得ることになってしまいます。

ちょうど前節の例でも、 DocumentSnapshot<unknown> | undefined が得られたとありますが、この unknownDocumentSnapshot<T> を解決できていないため生じるものです。こういった状況で型パラメータを渡したい場合は、型定義ファイルから依存を持ってきて手書きせざるを得ないようです。ここは将来のTypeScriptでの解決に期待したいところ。

こういった型パラメータを取りうる関数の場合は、注意する必要があるということも覚えておきましょう。

明日は Awaited<T>

本日は以上です。明日は『Awaited<T>』、本日の ReturnType<T> と組み合わせることでもっと面白いことができますよ。それではまた。

Discussion