GraphQL を使ったプロダクション開発では、GraphQL API とフロントエンド間の疎通に関しては型安全な状態を実現できる。しかし、GraphAPI の Resolver が叩く API のレスポンスについては当然ながら GraphQL の管理対象ではないため、この部分の型安全は保証されない。では GraphQL が型安全を担保してくれるわけではない Resolver 内で実行する API からのフェッチについて、型安全を保つためには、何が必要か。

様々な手法があると思われるが、今回は自分の実案件で導入した io-ts を用いる動的な型チェックを紹介する。これは通常 TypeScript がコンパイル時に行う静的型チェックではなく、実行時に行うことができる型チェックである。

https://codesandbox.io/s/admiring-edison-0j9qq?fontsize=14&hidenavigation=1&module=%2Fsrc%2Ftest%2Ftest2.test.ts&theme=dark

方針

テスト時にのみ io-ts を用いた実行時型チェックを行う。理由は、もしかしたら実行時型チェックを行うことでオーバーヘッドが大きくなり負荷が高まる可能性があるため。アプリケーション実行時の性能を下げることは、検証ができるまではやらないほうがいいと考えた。

今後、検証をし、負荷がそこまでなければ、プロダクト内でもこの型チェックを行い、問題があればエラーレポートを発行するようにする。これにより見つけにくい、API から取得した JSON の構造違いに起因するエラーを発見することが可能になる。

実装

io-tsfp-ts という関数型ライブリのエコシステムの上で動作するため、まず両者をパッケージに追加する。

yarn add fp-ts io-ts

では実際にテストを書いていく。

  • ShopUpdateResponse という型チェックをするバリデーターを定義する
  • ShopUpdateResponse.decode(data) でデータの型があっているかを実行時にチェックさせる
  • PathReporter.report(validationResult) とし、バリデーション結果に問題があるパスがあるかどうかをチェックする
  • 問題があるパスがわかる。例えば data.location.station が文字列じゃない、といったことがわかる。
import * as t from "io-ts";
import { PathReporter } from "io-ts/lib/PathReporter";

const Location = t.type({
  address: t.union([t.string, t.null]), // 住所
  station: t.union([t.string, t.null]) // 駅
});

const ShopUpdateResponse = t.type({
  name: t.string,
  description: t.union([t.string, t.null]),
  location: Location,
  images: t.array(t.string)
});

export type ResponseIoTs = t.TypeOf<typeof ShopUpdateResponse>;

test("型あっている", () => {
  const data: ResponseIoTs = {
    name: "name",
    description: null,
    location: { address: "address", station: "station" },
    images: ["image1"]
  };

  // 実行時にバリデーションする
  const validationResult = ShopUpdateResponse.decode(data);
  // チェックする
  console.log(PathReporter.report(validationResult));
  expect(PathReporter.report(validationResult)[0]).toEqual("No errors!");
});

ヴァリデーターから TS の型定義を作る

以下のコードは、ヴァリデーターから ResponseIoTs という TypeScript の型定義を抽出している。これを実際の開発に用いれば良い。(なお TypeScript の型定義からバリデーターを作る手法は基本的に用意されていない)

export type ResponseIoTs = t.TypeOf<typeof ShopUpdateResponse>;

型安全担保の方針

  • graph operation(query, mutation 等の実行): このレスポンスは GraphQL が担保する
  • resolver の実行: TS 用いてかつ codegen で型定義をすることで担保できる
  • resolver 内で API を叩いて値が返ってくるところ: ここで io-ts を使うこと。また io-ts のバリデーターから抽出した TS 型定義を開発で用いること