Closed7

GraphQLのTypeScript / React の型の自動生成

HaruHaru

フロントとして必要な型定義、またあると便利な型定義は下記三つ。

  1. スキーマ(SDL)ファイルからTypeScriptの型の自動生成
  2. フロントの定義による、リクエストとレスポンスなどの型の自動生成
  3. Apollo ClientやReact Query用のHooksの自動生成
HaruHaru

下記の ./schema.graphql ファイルが、ディレクトリのルートに存在するとする

type Author {
  id: Int!
  firstName: String!
  lastName: String!
  posts(findTitle: String): [Post]
}

type Post {
  id: Int!
  title: String!
  author: Author
}

type Query {
  posts: [Post]
}
HaruHaru

1. スキーマ(SDL)ファイルからTypeScriptの型の自動生成

codegen.yml には最低限の内容。
schemaの指定と出力先の指定のみ。

// ./codegen.yml
schema: ./schema.graphql
generates:
  ./src/models.ts:
    plugins:
      - typescript

./src/models.ts に、下記のように ./schema.graphql に定義されているスキーマの全種類の型が出力される。

// ./src/models.ts
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
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]> };
/** 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 Author = {
  __typename?: 'Author';
  firstName: Scalars['String'];
  id: Scalars['Int'];
  lastName: Scalars['String'];
  posts?: Maybe<Array<Maybe<Post>>>;
};


export type AuthorPostsArgs = {
  findTitle?: InputMaybe<Scalars['String']>;
};

export type Post = {
  __typename?: 'Post';
  author?: Maybe<Author>;
  id: Scalars['Int'];
  title: Scalars['String'];
};

export type Query = {
  __typename?: 'Query';
  posts?: Maybe<Array<Maybe<Post>>>;
};
HaruHaru

2.フロントの定義による、リクエストとレスポンスなどの型の自動生成

下記の ./queries/posts.ts のように、ディレクトリルートの ./queries/ ディレクトリにqueryファイルが存在するとする。(フロント側による定義)

// ./queries/posts.ts
import {gql} from '@apollo/client';

export const PostsDocument = gql`
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`;

codegen.yml のdocumentsに上記の queries ディレクトリを指定する

// ./codegen.yml
schema: ./schema.graphql
documents: ./queries
generates:
  ./src/models.ts:
    - typescript
    - typescript-operations

./src/models.ts に上述の型定義に合わせて、下記のPostsQuery が出力される

// ./src/models.ts
export type PostsQueryVariables = Exact<{ [key: string]: never; }>;

export type PostsQuery = { __typename?: 'Query', posts?: Array<{ __typename?: 'Post', id: number, title: string, author?: { __typename?: 'Author', id: number, firstName: string, lastName: string } | null } | null> | null };

使用例: 自動生成された型をレスポンスの型に指定し使用する

import {useQuery} from '@apollo/client';
import {PostsQuery} from "./src/models.ts";
import {PostsDocument} from "./src/queries/posts.ts"

const {loading, data, error} = useQuery<PostsQuery>(PostsDocument);
HaruHaru

3.Apollo ClientやReact Query用のHooksの自動生成

2番のようにGenericsを指定するのではなく、リクエストやレスポンスの型が定義されているhooksを使用する場合。
codegen.yml にapolloのpluginを追加

// ./codegen.yml
schema: ./schema.graphql
documents: ./queries
generates:
  ./src/models.ts:
    - typescript
    - typescript-operations
    - typescript-react-apollo

上述の型定義に合わせて、下記の型定義も出力される

// ./src/models.ts
export const PostsDocument = gql`
    query Posts {
  posts {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}
    `;

/**
 * __usePostsQuery__
 *
 * To run a query within a React component, call `usePostsQuery` and pass it any options that fit your needs.
 * When your component renders, `usePostsQuery` 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 } = usePostsQuery({
 *   variables: {
 *   },
 * });
 */
export function usePostsQuery(baseOptions?: Apollo.QueryHookOptions<PostsQuery, PostsQueryVariables>) {
        const options = {...defaultOptions, ...baseOptions}
        return Apollo.useQuery<PostsQuery, PostsQueryVariables>(PostsDocument, options);
      }
export function usePostsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PostsQuery, PostsQueryVariables>) {
          const options = {...defaultOptions, ...baseOptions}
          return Apollo.useLazyQuery<PostsQuery, PostsQueryVariables>(PostsDocument, options);
        }
export type PostsQueryHookResult = ReturnType<typeof usePostsQuery>;
export type PostsLazyQueryHookResult = ReturnType<typeof usePostsLazyQuery>;
export type PostsQueryResult = Apollo.QueryResult<PostsQuery, PostsQueryVariables>;

下記のように usePostsQuery として各種型が指定されているhooksを使用可能となる。

import {usePostsQuery} from "./src/models.ts";

const {loading, data, error} = usePostsQuery()

typescript-react-apollotypescript-react-query のプラグインに置き換えれば、React Queryも同様に出力される

// ./codegen.yml
schema: ./schema.graphql
documents: ./queries
generates:
  ./src/models.ts:
    - typescript
    - typescript-operations
    - typescript-react-query
HaruHaru

@graphql-codegen/typed-document-node

Apollo Client対応だと、よりよいプラグインがあると紹介されている。
useQueryをそのまま使用させつつ、型推論をさせる。

low bundle impact (compared to other plugins)
better backward compatibility (easier to migrate to)
more flexible

プラグインは typed-document-node に変更。

// ./codegen.yml
schema: ./schema.graphql
documents: ./queries
generates:
  ./src/models.ts:
    - typescript
    - typescript-operations
    - typed-document-node

今回新たに自動出力された内容

// ./src/models.ts
export type PostsQueryVariables = Exact<{ [key: string]: never; }>;


export type PostsQuery = { __typename?: 'Query', posts?: Array<{ __typename?: 'Post', id: number, title: string, author?: { __typename?: 'Author', id: number, firstName: string, lastName: string } | null } | null> | null };


export const PostsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Posts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"posts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}}]}}]}}]} as unknown as DocumentNode<PostsQuery, PostsQueryVariables>;

Apollo ClientのuseQueryはそのまま使用。
自動出力されたDocumentの定義によって、型推論させる。

import {useQuery} from '@apollo/client';
import {PostsDocument} from "./src/models.ts";

const {loading, data, error} = useQuery(PostsDocument)
このスクラップは2023/01/23にクローズされました