Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

GraphQL + TypeScript への課題感

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

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

やり方

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

https://graphql-code-generator.com

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

├── 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を書きました。

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 の練習だったんですが…)

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

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 に型がつき、返り値にも型を要求されます

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

//...

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 に注目してください

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() を発行しています。

感想

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

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

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

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

prisma - 最速 GraphQL Server 実装 - Qiita

plaid
CXプラットフォーム「KARTE」の開発・運営、EC特化型メディア「Shopping Tribe」の企画・運営、CX特化型メディア「XD(クロスディー)」の企画・運営
https://plaid.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした