graphql-codegen で型定義を生成する (React, Apollo, TypeScript)

本記事はこのリポジトリでやったことのまとめです。

https://github.com/mizchi-sandbox/graphql-react-apollo-playground

## GraphQL + TypeScript への課題感

TypeScript(に限らず他の静的型付の言語) と GraphQL を使うと、型の二重定義が発生がちです。折角 GraphQL に通信規約としての型を書いているのに、それを多重定義することで、運用の面倒臭さやバグの温床になりかねない、という懸念がありました。

今回は、graphql のスキーマとクエリを書くと、サーバー向けに resolver の型定義、クライアント向けにクエリの型定義を生成し、それによってできるかぎり型安全なコードを扱うのをゴールとします。

## やり方

graphql-code-generator を使います。(というか今回は主にこのライブラリの紹介です)

https://graphql-code-generator.com

期待してるディレクトリ構造はこんな感じ

```bash

├── graphql
│   ├── mutations
│   │   ├── addUser.graphql
│   │   └── deleteUser.graphql
│   ├── queries
│   │   ├── user.graphql
│   │   └── users.graphql
│   └── schema.graphql
├── client
│   ├── index.html
│   └── main.tsx
├── server
│   ├── index.ts
│   ├── package.json
│   └── resolvers.ts
└── codegen.yml

```

ごちゃごちゃしてますが、

`graphql` にスキーマ定義を
`client` が クライアント実装を
`server` が GraphQL サーバー実装

です。

## 型を生成する

`server/gen` に graphql resolver の型定義を、 `client/gen` に graphql の query と mutation の hooks API を生成するとします。

パッケージが細かく別れていて、依存がやたら多いですが、この辺をインストールします。

```
yarn add -D @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-resolvers @graphql-codegen/typescript-react-apollo
```

`yarn graphql-codegen init` で生成してもいいですが、今回は次のような `codegen.yml`を書きました。

```yaml
overwrite: true
schema:
  - ./graphql/schema.graphql
documents:
  - ./graphql/queries/*.graphql
  - ./graphql/mutations/*.graphql
generates:
  ./server/gen/graphql-resolver-types.ts:
    plugins:
      - typescript
      - typescript-resolvers
  ./client/gen/graphql-client-api.tsx:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withComponent: false
      withHooks: true
      withHOC: false
  ./graphql/schema.json:
    plugins:
      - introspection
```

- typescript-resolver を使ったリゾルバ用型定義
- typescript-react-apollo を使った React 向け型定義
- schema.json

の吐き出すことを意図してます。

yarn 経由でコマンドを叩いてコード生成

```
$ yarn graphql-codegen --config codegen.yml
  ✔ Parse configuration
  ✔ Generate outputs
```

どういうコードが生成されたかは、使いながら確認していきます。

## サーバーサイドのリゾルバーの型定義

typeorm を使った簡単な CRUD のサンプルです。(というか正直言うと今回作ったリポジトリは typeorm の練習だったんですが…)

生成された型定義によって `MutationResolvers` と `QueryResolvers` が手に入るので、これによってリゾルバの実装に型をつけることができます。

```typescript
import { ulid } from 'ulid';
import {
  MutationResolvers,
  QueryResolvers,
  Resolvers,
} from './gen/graphql-resolver-types';
import { User } from './entity/User';

const Query: QueryResolvers = {
  async user(_parent, args, _context, _info) {
    const user = await User.findOne({ id: args.id });
    return user || null;
  },
  async users() {
    const users = await User.find();
    return users;
  },
};

const Mutation: MutationResolvers = {
  async addUser(_parent, args, _context, _info) {
    const newUser = new User();
    newUser.id = ulid();
    newUser.name = args.name;
    await User.save(newUser);
    return newUser;
  },
  async deleteUser(_parent, args, _context, _info) {
    const user = await User.findOne(args.id);
    await User.delete(args.id);
    return user;
  },
};

export const resolvers: Resolvers = {
  Query,
  Mutation,
};
```

VSCode などで触ればわかるのですが、第二引数の args に型がつき、返り値にも型を要求されます

生成コードを見ると、次のようなコードが生成されているのがわかります。

```typescript
//...

export type MutationResolvers<Context = any, ParentType = Mutation> = {
  addUser?: Resolver<User, ParentType, Context, MutationAddUserArgs>,
  deleteUser?: Resolver<User, ParentType, Context, MutationDeleteUserArgs>,
};

export type QueryResolvers<Context = any, ParentType = Query> = {
  user?: Resolver<Maybe<User>, ParentType, Context, QueryUserArgs>,
  users?: Resolver<Maybe<Array<User>>, ParentType, Context>,
};
```

これで resolver を安心して書くことが出来るようになりました。

## クライアントで生成されたコードを使う

↑ で作成された graphql server を `http://localhost:3333` で動かすのを前提としています。
各種セットアップは略。リポジトリを見てください。手を抜くのに parcel を使っています。

生成されたコードをどう使っているか、次のコードの `import ... from './gen/graphql-client-api'` と UserList の hooks に注目してください

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloProvider } from 'react-apollo';
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks';
import {
  useUsersQuery,
  useAddUserMutation,
  useDeleteUserMutation,
} from './gen/graphql-client-api';

const client = new ApolloClient({
  link: createHttpLink({
    uri: 'http://localhost:3333/graphql',
  }),
  cache: new InMemoryCache(),
});


function UserList() {
  const usersQuery = useUsersQuery();
  const addUserMutation = useAddUserMutation();
  const deleteUserMutation = useDeleteUserMutation();

  return (
    <>
      <h1>Users</h1>
      <ul>
        {!usersQuery.loading &&
          usersQuery.data.users.map(user => {
            return (
              <li key={user.id}>
                {user.id}:{user.name}
                <button
                  onClick={async () => {
                    await deleteUserMutation({ variables: { id: user.id } });
                    usersQuery.refetch();
                  }}
                >
                  delete
                </button>
              </li>
            );
          })}
      </ul>
      <button
        onClick={async () => {
          await addUserMutation({
            variables: { name: Math.random().toString() },
          });
          usersQuery.refetch();
        }}
      >
        addUser
      </button>
    </>
  );
}

function App() {
  return (
    <ApolloProvider client={client}>
      <ApolloHooksProvider client={client}>
        <UserList />
      </ApolloHooksProvider>
    </ApolloProvider>
  );
}

ReactDOM.render(<App />, document.querySelector('.root'));
```

副作用を起こしたら `usersQuery.refetch()` を発行しています。

![](https://i.gyazo.com/237cff1065cc287dc51e2a1e7bf99242.gif)

## 感想

スキーマからコードを生成したことで、自分で定義した部分は最小限で、かつ TypeScript の型チェックを効かせることができました。

GraphQLと型、なんか全体的にもったいない感覚がしてたのが、これによってだいぶ楽になった気がしていて、やっとプロダクションに突っ込むやる気が出てきた気がします。

厳密には、typeorm と GraphQL 間で依然として二重定義のような部分は残ってるのですが、ここは GraphQL は ORM ではないという点を鑑みて、分離したままにしておいたほうがいいと思っています。

GraphQL を ORM として使いたいなら prisma を使うのが楽です。

[prisma - 最速 GraphQL Server 実装 - Qiita](https://qiita.com/mizchi/items/74fb8b1177e83fe10273)