thumbnail

GraphQL APIのTypescriptの型をClient側で自動生成する

2021/09/05

openapi2aspidaみたいにswagger.ymlからtypescriptの型を自動生成してくれるのが便利すぎて、GraphQLでもgraphqlファイルから型を自動生成するやつないかな〜と思っていたら普通にありました。 その名もGraphQL Code Genratorです。 今回はGraphQL Code Generatorを使わない場合と使う場合で実際のコードがどう変化するのかを見ながら、その便利さを紹介できたらと思います。 ちなみにフレームワークはReact + Apollo Clientを想定しています。

Apollo Client with Typescript

GraphQL Code Generatorを使用しない場合

Apollo ClientをReactで使用する場合は通常、以下のようなコードになります。

index.tsx
import React from 'react';
import { useQuery, gql } from '@apollo/client';

type User = {
  id: number;
  age: number;
  name: string;
};

type UserData = {
  users: User[];
};

type UserVars = {
  age: number;
};

const GET_USERS = gql`
  query GetUsers($age: !Int) {
    users(age: $age) {
      id
      age
      name
    }
  }
`;

export const UserList: React.FC = () => {
  const { data } = useQuery<UserData, UserVars>(GET_USERS, {
    variables: { age: 28 },
  });

  return (
    <ul>
      {data.users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
};

十分実用的ではありますが、やはりUserDataとかUserVarとかの型定義が若干めんどくさいですね... めんどくさがり屋の自分としては

index.tsx
const GET_USERS = gql`
  query GetUsers($age: !Int) {
    users(age: $age) {
      id
      age
      name
    }
  }
`;

からある程度の型は推測してほしいと思ってしまいます🥺

GraphQL Code Generatorを使用した場合

では続いてGraphQL Code Generatorを使用した場合どうなるか見てみます。 以下のようなschema.graphqlが用意できてるとして進めます。

schema.graphql
type User {
  id: Int!
  name: String!
  age: Int!
}

type Query {
  users(age: Int!): [User!]!
}

使用したいqueryを書いたgraphqlファイルを用意して

user.graphql
query users($age: Int!) {
  users(age: $age) {
    id
    age
    name
  }
}

以下のGraphQL Code GeneratorのCLIコマンドを実行すると...

shell
$ npx graphql-codegen

次のような型情報が入ったtypescriptファイルが生成されます。 ※ 実行時にschema.graphqluser.graphqlの間に不整合があった場合はエラーが発生するのでご注意ください(不整合チェックに使えて便利👏)。

generated.ts (Clickで詳細)
generated.ts
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
const defaultOptions =  {}
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

export type Query = {
  __typename?: 'Query';
  users: Array<User>;
};

export type QueryUsersArgs = {
  age: Scalars['Int'];
};

export type User = {
  __typename?: 'User';
  id: Scalars['Int'];
  name: Scalars['String'];
  age: Scalars['Int'];
};

export type UsersQueryVariables = Exact<{
  age: Scalars['Int'];
}>;

export type UsersQuery = { __typename?: 'Query', users: Array<{ __typename?: 'User', id: number, age: number, name: string }> };

export const UsersDocument = gql`
    query users($age: Int!) {
  users(age: $age) {
    id
    age
    name
  }
}
    `;

/**
 * __useUsersQuery__
 *
 * To run a query within a React component, call `useUsersQuery` and pass it any options that fit your needs.
 * When your component renders, `useUsersQuery` returns an object from Apollo Client that contains loading, error, and data properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
 *
 * @example
 * const { data, loading, error } = useUsersQuery({
 *   variables: {
 *      age: // value for 'age'
 *   },
 * });
 */
export function useUsersQuery(baseOptions: Apollo.QueryHookOptions<UsersQuery, UsersQueryVariables>) {
        const options = {...defaultOptions, ...baseOptions}
        return Apollo.useQuery<UsersQuery, UsersQueryVariables>(UsersDocument, options);
      }
export function useUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<UsersQuery, UsersQueryVariables>) {
          const options = {...defaultOptions, ...baseOptions}
          return Apollo.useLazyQuery<UsersQuery, UsersQueryVariables>(UsersDocument, options);
        }
export type UsersQueryHookResult = ReturnType<typeof useUsersQuery>;
export type UsersLazyQueryHookResult = ReturnType<typeof useUsersLazyQuery>;
export type UsersQueryResult = Apollo.QueryResult<UsersQuery, UsersQueryVariables>;

※ 実際にnpx graphql-codegenで上記のような型ファイルを生成するには、事前にcodegen.ymlファイルを用意しておく必要があります(具体的には後述の導入方法のsectionに記載しています)。
生成されたtypescriptファイルをimportして最初のtsxファイルを以下のように書くことができます。

index.tsx
import React from 'react';
import { useUsersQuery } from './graphql/generated';

export const UserList: React.FC = () => {
  // dataやvariablesの型もしっかり定義されている
  const { data } = useUsersQuery({ variables: { age: 28 } });

  return (
    <ul>
      {data.users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
};

型を自分で定義する必要がなくなったのでだいぶスッキリしました✨

GraphQL Code Generator導入方法

GraphQL Code Generatorの導入方法ですが、Apollo Clientを使用する場合、必要なパッケージは以下の通りです。

  • @graphql-codegen/cli
    coreパッケージ
  • @graphql-codegen/typescript
    typexcriptの型生成する場合に必用なプラグイン
  • @graphql-codegen/typescript-operations
    GraphQL のクエリとスキーマを元にtypescriptの型を自動生成するプラグイン
  • @graphql-codegen/typescript-react-apollo
    生成されたファイルから直接apollo/clientを使えるようにするプラグイン
shell
$ yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

続いて以下のcodegen.ymlファイルをプロジェクト直下に用意します。

codegen.yml
schema: ./path/to/schema.graphql
documents:
  # user.graphqlなどのqueryやmutationが書かれたファイルを指定
  - ./graphql/**/*.graphql
generates:
  graphql/generated.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

これで導入は完了です。 npx graphql-codegenを実行すると型情報が記載されたtsファイルが生成されると思います。

おわり

APIの型は自動生成する時代 🏄‍♂️

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

Profile

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

© 2022 mitsuru takahashi