4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Amplify]自動生成GraphQLクエリの型解決について

Last updated at Posted at 2021-02-08

Amplify CodeGenは、定義したGraphQLスキーマから自動でクエリを生成してくれます。
ただ、AmplifyライブラリのGraphQLAPIからクエリを実行すると戻り値型はanyになります。
今回はTypescriptで開発する際に、自動生成クエリの実行結果に型を持たせる方法について記載します。
結論だけみたい方は、ステップ3だけをご覧ください。

AmplifyでGraphQLクエリを実行する場合

amplifyでは、graphQLクエリ実行用のAPIを用意してくれています。

import API, { graphqlOperation } from '@aws-amplify/api';
API.graphql(graphqlOperation(query,variables));

例えば、以下のように実行できます。

import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ 
const res = API.graphql(graphqlOperation(getPost,{id:"XXX"}));
/* res
data:{
  getPost:{
    id: XXX,
    title: "XXブログ"
  }
}
*/

上記のようにクエリ実行結果を得ることができますが、結果の型はanyになります。
もちろん、InterfaceやTypeを独自定義すれば、型をつけることはできます。

type Post{
  id: string;
  title: string;
}

ただ、開発中にGraphQLスキーマを見直したいことも多く、その度に型定義も修正するのは少し面倒です。また、他モデルを参照するモデルの自動生成クエリは、以下のように階層構造になっているので、全てを独自定義するのは至難です。

@/graphql/queries.ts
export const getPost = /* GraphQL */ 
  query GetPost($id: ID!) {
    getPost(id: $id) {
      id
      title
      comments {
        items {
          id
          content
        }
      }
   }
};

そこで、このような階層も型定義可能な方法を考えてみます。

ステップ1(API.tsの利用)

CodeGenを実行すると以下のような定義ファイル(API.ts)が生成されます。

@/API.ts
export type GetPostQueryVariables = {
  id: string,
};

export type GetPostQuery = {
  getPost:  {
    __typename: "Post",
    id: string,
    title: string | null,
    comment:  {
      __typename: "comment",
      id: string,
      content: string | null,
    } | null,
  } | null,
};

これを使うと以下のように型解決ができそうです。

import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ 
const res: GetPostQuery = API.graphql(graphqlOperation(getPost,{id:"XXX"})).data;

これで良いと思ったのですが、2点気になります。

課題:__typenameが含まれる

これはあった方がありがたいのでは?と思ったのですが、型定義上だけでクエリ実行結果には含まれません。

import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ 
const res: GetPostQuery = API.graphql(graphqlOperation(getPost,{id:"XXX"})).data;
console.log(res.__typename) // undefined

無視すれば実害はないのですが、型と値に差異があるのは開発を少し混乱させます。

課題:Optionalになる

これは本来正しいことなので気にしない方がいいかもしれません。
ただ、シンプルなデータ型を作りたいだけであれば、取り除きたい場合もあるかもしれません。

import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ 
const res: GetPostQuery = API.graphql(graphqlOperation(getPost,{id:"XXX"})).data;
console.log(res.id) // error: res is possibly null
// Optionalなので以下のような実装が必要
if(res) console.log(res.id) //対策1
console.log(res?.id) // 対策2

ステップ2(型定義の編集)

上記のような課題を踏まえて、以下記事では__typenameとOptionalの除去を行っています。
(Optionalについては除去するが、使用時はちゃんとNullチェックしていますね)
https://medium.com/@dantasfiles/using-typescript-with-aws-amplify-api-3788d722869

import API from '@aws-amplify/api';
type Post= Omit<Exclude<API.GetPostQuery['getPost'], null>,
                 '__typename'>;

順番に説明します。
Exclude<T,U>は、型Tから型Uに代入可能な属性を取り除きます。
GetPostQuery['getPost']は、{...} | nullであるため、{...}となります。
Omit<T,K>は、型Tから属性Kを取り除きます。
なので{...}から属性__typenameを取り除きます。

結果、以下のような型が出来上がります。

type Post = {
   id: string,
   titlte: string | null,
   ・・・
}

シンプルなデータ型のようになっていますね。
これでOKですが、まだ少し気になる点があります。

課題:複数モデルが結合する場合が考慮されていない

PostがCommentを持つようなデータモデルの場合、どうなるでしょう。

type Post = {
   id: string,
   titlte: string | null,
   comments: {
     __typename: "ModelCommentConnection",
     items: Array<{
       __typename: "comment",
       id: string,
       content: string,
     } | null> | null
   } | null
}

commentsの中は、__typenameが残り、Optionalな状態になります。

ステップ3(再帰的型定義の編集)

上記を踏まえて、追加の型編集を行います。
考え方としては、前ステップを再帰的に各層に適用するというものです。

type Deep<T> = 
    T extends any[] ? DeepOmitArray<T[number]>:
    T extends object ? DeepOmitObject<T>:
    T;
type DeepOmitArray<T> = Array<Omit<Exclude<Deep<T>,null>,'__typename'>>
type DeepOmitObject<T> = {
    [P in keyof Omit<Exclude<T,null>,'__typename'>]: Deep<Omit<Exclude<T,null>,'__typename'>[P]>;
}

Deep<T>は、型Tの内容に応じて型を決定します。
配列、オブジェクト、それ以外の分岐になっています。
配列の場合、DeepOmitArray<T>型となります。
これはArray<Omit<Exclude<Deep<T>,null>,'__typename'>>としていますが、配列の中身をDeep<T>に渡して、結果にステップ2の型編集を当てるというものです。
これにより、Tの中身が配列・オブジェクト以外になるまで再帰的に型解決が行われます。
オブジェクトの場合、DeepOmitObject<T>型となります。
考え方は配列と同様で、オブジェクトの各属性毎にDeep<T>に渡して、再帰的な型解決を行います。

これにより、最終的には以下のような型が得られます。

type Post = {
   id: string,
   titlte: string | null,
   comments: {
     items: Array<{
       id: string,
       content: string,
     }>
   }
}

comments配下にも__typename除去・optional除去が効いたかと思います。

おわりに

本記事では、Typescript Utility Typesを活用して自動生成GraqhQLクエリの型解決を行いました。
今回はOptionalも除去しましたが、本来は残しておくべきな気もします。使い方に応じて柔軟に実装できればと思います。
まだ不十分な点もあるかと思いますので、指摘などあればいただければ幸いです。

また、検討にあたり、参考にさせていただいた以下記事には大変感謝いたします。
https://medium.com/@dantasfiles/using-typescript-with-aws-amplify-api-3788d722869

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?