io-ts を用いて RESTful API のレスポンスの型があっているかチェックする
TweetGraphQL を使ったプロダクション開発では、GraphQL API とフロントエンド間の疎通に関しては型安全な状態を実現できる。しかし、GraphAPI の Resolver が叩く API のレスポンスについては当然ながら GraphQL の管理対象ではないため、この部分の型安全は保証されない。では GraphQL が型安全を担保してくれるわけではない Resolver 内で実行する API からのフェッチについて、型安全を保つためには、何が必要か。
様々な手法があると思われるが、今回は自分の実案件で導入した io-ts
を用いる動的な型チェックを紹介する。これは通常 TypeScript がコンパイル時に行う静的型チェックではなく、実行時に行うことができる型チェックである。
方針
テスト時にのみ io-ts を用いた実行時型チェックを行う。理由は、もしかしたら実行時型チェックを行うことでオーバーヘッドが大きくなり負荷が高まる可能性があるため。アプリケーション実行時の性能を下げることは、検証ができるまではやらないほうがいいと考えた。
今後、検証をし、負荷がそこまでなければ、プロダクト内でもこの型チェックを行い、問題があればエラーレポートを発行するようにする。これにより見つけにくい、API から取得した JSON の構造違いに起因するエラーを発見することが可能になる。
実装
io-ts
は fp-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 型定義を開発で用いること