$shibayu36->blog;

エンジニアをしています。プログラミングや読書のことなどについて書いています。

TypeScript + Apollo ClientでGraphQLのデータに型を付ける

TypeScript + Apollo Clientで、useQueryなどを用いてGraphQLのクエリを発行する際に、クエリのvariablesやレスポンスのデータに型を付けたい。やり方が少々分かりづらかったのでメモを残す。

型をつけるためにやることは以下の通り。

  • apollo.config.jsを定義する
  • graphql-tagでGraphQLのクエリを書く
  • apollo client:codegenで型情報を生成する
  • クエリを書いた場所で型をimportし、useQueryの型パラメータに指定する

shibayu36/go-Intern-Diaryのレポジトリで試している。

apollo.config.jsを定義する

まずApolloのコードジェネレータが適切に動作するために、apollo.config.jsを作る。Configuring Apollo projects - Apollo Basics - Apollo GraphQL Docsを参考にする。

今回の場合、GraphQLのスキーマはGraphQLエンドポイントから取得し、クライアントから発行するクエリは./ui/src/以下のファイルに定義するので、次の内容になる。

module.exports = {
  client: {
    includes: ['./ui/src/**/*.tsx', './ui/src/**/*.ts'],
    service: {
      name: "go-Intern-Diary",
      url: "http://localhost:8000/query"
    }
  }
};

graphql-tagでGraphQLのクエリを書く

続いてクライアント側でgraphql-tagを使ってクエリを書く。クエリに付けた名前が型情報の名称に使われるため、必ずクエリにプロジェクトでユニークな名前を付けておく。

ui/src/pages/diary.tsx

import gql from "graphql-tag";

...

const getDiaryQuery = gql`
  query GetDiary($diaryId: ID!) {
    diary(diaryId: $diaryId) {
      id
      name
    }
  }
`

apollo client:codegenで型情報を生成する

apollo.config.jsを用意し、graphql-tagでクエリを書いていれば、apollo client:codegenコマンド一つで型情報を生成できる。useReadonlyTypesオプションを使うと全てreadonlyで型情報が作成されるため、意図しない代入を防げる。好みに合わせて--outputFlatも使うと良さそう。

npx apollo client:codegen --useReadOnlyTypes --target=typescript

これでui/src/pages/__generated__/GetDiary.tsのように、クエリ名がファイル名となり型情報が出力される。

クエリを書いた場所で型をimportし、useQueryの型パラメータに指定する

あとは上で出力された型情報をimportし、useQueryの型パラメータに指定する。

ui/src/pages/diary.tsx

import { GetDiary, GetDiaryVariables } from "./__generated__/GetDiary";

...

const { loading, error, data } = useQuery<GetDiary, GetDiaryVariables>(
  getDiaryQuery,
  {
    variables: { diaryId: diaryId }
  }
);

これでdata変数はGetDiary型となり、変なプロパティにアクセスするとコンパイルエラーが発生するようになる。またvariablesもdiaryId以外のプロパティを指定するとコンパイルエラーになる。間違った使い方にすぐに気付けるようになって安心。

ちなみに、useQueryはloadingやerrorが返ってくる時dataはundefinedとなるため、data.diaryとかするとObject is possibly 'undefined'のようにコンパイルエラーになってしまう。先にloadingやerrorのチェックをしていれば、dataは存在するとみなせるはずなので、!.演算子を使うと良さそう(参考: tslint - In Typescript, what is the ! (exclamation mark / bang) operator when dereferencing a member? - Stack Overflow)。

ui/src/pages/diary.tsx

if (loading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;

return <div className="Diary">
  <h1>{data!.diary.name}</h1>
</div>;

参考: variablesが存在しない時のやり方

クエリにvariablesが存在しない時はuseQueryの型パラメータの一番目のみ指定すれば良い。

ui/src/pages/diaries.tsx

const { loading, error, data } = useQuery<GetMyDiaries>(getMyDiariesQuery);

まとめ

useQueryの型の付け方や、client:codegenの使い方がドキュメントからだとよく分からずに結構ハマってしまった。今回で理解できたので、ようやくGraphQLのクエリをバリバリ書けそう。