Chapter 10

玖ノ型:約束の地へ

Satoshi Takeda
Satoshi Takeda
2020.12.03に更新

やっぱり型を書きたくないんだ

型の話ばかりでだんだん食傷気味になっていませんか。ジェネリクスや Conditional Type などをプロダクトコードで頑張って書く必要はもちろんありません。

ここではこれまでの活用方法を手を抜くために使ってみるというのをご提案します。

Next.js の Props 定義で手を抜く

Next.js における活用例を見てみます。まず Next.js がどういったものを話すべきでしょうが、Next.js について触れた記事はたくさんあるので各々見ていただくとしてある程度かいつまみます。実装するシーンとしては下記のようなイメージです。

  • SSG でビルドするため getStaticProps を利用している
  • page/index.ts でページコンポーネントには必要な Props が多い
  • Props はほとんどが Web API をデータソースとしている

コードイメージとしてはページコンポーネントの Props を人間がかき集めて定義しているにもかかわらず、getStaticProps の戻り値には Props 同様の型情報が含まれており重複している状況を想像してください。つまり Web API などの外部リソースから得られる型情報が自明であるにもかかわらず、ページコンポーネントの Props を人間が手動で同じような型定義しなくてはならないので非常にかったるいわけです。

src/pages/index.tsx
type Article = {/* 省略 */}
type Category = {/* 省略 */}
type User = {/* 省略 */}
type Ranking = {/* 省略 */}
type Props = {
  articles: Article[]
  categories: Category[]
  ranking: Ranking
}

const Index: NextPage<Props> = (props) => {
  // そこそこの粒度のコンポーネント群
}

export const getStaticProps = async () => {
  const client = new Fetcher()
  const articleRepo = new ArticleRepository(client)
  const categoryRepo = new CategoryRepository(client)
  const rankingRepo = new RankingRepository(client)
  const articles = await articleRepo.findAll()
  const categories = await categoryRepo.findAll()
  const ranking = await rankingRepo.findAll()
  return {
    props: {
      articles,
      categories,
      ranking,
    }
  }
}

export default Index

Props に定義した各プロパティである articles, categories, rankinggetStaticProps の戻り値に含まれ型が確定しています(と仮定します)。すでに型が自明であるのに Props をあらためて定義したくないというのは誰しも起こる感情です。恥ずかしくありません。

上記例では *Repository にあたる、リソースを集約したオブジェクト取得のためのクラスモジュールには、データソースの型定義や集約された型が同梱されていることでしょう。その型定義を Props として再利用する方法も候補にはなりそうです。

しかし、要件により getStaticProps 内部で取得したオブジェクトをビュー向けに加工する必要が出てくるとしたらどうでしょうか。外部モジュールで定義された型を安易に Props として利用するのはあまり得策とは言えません。

Conditional Type と型推論

そこでここまで登場した ReturnType を活用して getStaticProps の戻り値導出を考えてみます。

type Props = ReturnType<typeof getStaticProps>
//  type Props = Promise<...>

残念ながら getStaticProps は Promise を返却するため、実際には fullfilled になった結果を得ないと Props として渡すことは難しそうです。ここで Conditional Types を持ち出しましょう。分解しやすいように細かくちぎります。

type GetStaticPropsPromise = ReturnType<typeof getStaticProps>
// type GetStaticPropsPromise = Promise<{props: {期待する Props 群}}>

type GetStaticPropsValue = GetStaticPropsPromise extends Promise<infer R> ? R : never
// type GetStaticPropsValue = {props: {期待する Props 群}

type Props = GetStaticPropsValue["props"]
// type Props = {期待する Props 群}

結果から言うと Props はここでようやく欲しい型定義 {期待する Props 群} になります。順に解説します。

見慣れない infer というのが出てきました。infer は Conditional Type における条件節で利用でき、実際の条件で比較する型(ここでは Promise<{props: {期待する Props 群}}>)から導きたい型を推論できるキーワードです。infer R と指定された新たな型変数 R は戻り値で利用され、ここでは {props: {期待する Props 群}} が導出されるといった具合です。

getStaticProps は実装者が大きな変更をしない限り Promise を返却するので、条件は必ず真に流れるため偽の場合は型を返さない(never)という書き方をしています。

あとは必要な props を抜き取るだけで Props には欲しい型が入ります。ここまでの一連を一筆書きするとすれば下記のようになるでしょう。ただしこれがチームで読めるかどうかは習熟度によりそうです。自分ならこういった記述は多用しません。

type Props = Pick<ReturnType<typeof getStaticProps> extends Promise<infer R> ? R : never, "props">

簡単ですが、関心のある型を再生産している・型の構造を追う必要があるといった手間から解き放たれるための、導出の一例を紹介しました。

布を覆う

ようやく布で覆っていきます。伊之助と若干色が違う? 毛束感がない? それは予算の関係です。ユザワヤの、フェイクファーのメーター単位の相場を知ってから言ってください。話はそれからだ。ここは値段と相談しての茶色のベロアだったというところで落ち着きましょう。

ざっくりと布を切ってかぶせます。まずは気にせずざっくりとかぶせてから調整を考えてよいです。接着にはここでもボンド G10 が登場します。強力なスプレーのりでもよいのですが、粘着が弱く貼りつかないことも多かったので経験上ボンドにしています。

覆った布を少しずつ端から接着していきます。丁寧にやらないとよれたりシワになったりするので慎重にやりましょう。布終端まで貼り終えるとピンとした状態で勇ましい印象になりますね。