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

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

## AmplifyでGraphQLクエリを実行する場合
amplifyでは、graphQLクエリ実行用のAPIを用意してくれています。

```typescript
import API, { graphqlOperation } from '@aws-amplify/api';
API.graphql(graphqlOperation(query,variables));
```
例えば、以下のように実行できます。

```typescript
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を独自定義すれば、型をつけることはできます。

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

```typescript:@/graphql/queries.ts
export const getPost = /* GraphQL */ 
  query GetPost($id: ID!) {
    getPost(id: $id) {
      id
      title
      comments {
        items {
          id
          content
        }
      }
   }
};
```
そこで、このような階層も型定義可能な方法を考えてみます。

## ステップ１（API.tsの利用）
CodeGenを実行すると以下のような定義ファイル(API.ts)が生成されます。

```typescript:@/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,
};
```

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

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

これで良いと思ったのですが、２点気になります。
#### 課題：__typenameが含まれる
これはあった方がありがたいのでは？と思ったのですが、型定義上だけでクエリ実行結果には含まれません。

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

```typescript
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

```typescript
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`を取り除きます。

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

```typescript
type Post = {
   id: string,
   titlte: string | null,
   ・・・
}
```
シンプルなデータ型のようになっていますね。
これでOKですが、まだ少し気になる点があります。

#### 課題：複数モデルが結合する場合が考慮されていない
PostがCommentを持つようなデータモデルの場合、どうなるでしょう。

```typescript
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（再帰的型定義の編集）
上記を踏まえて、追加の型編集を行います。
考え方としては、前ステップを再帰的に各層に適用するというものです。

```typescript
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>`に渡して、結果にステップ２の型編集を当てるというものです。
これにより、Tの中身が配列・オブジェクト以外になるまで再帰的に型解決が行われます。
オブジェクトの場合、`DeepOmitObject<T>`型となります。
考え方は配列と同様で、オブジェクトの各属性毎に`Deep<T>`に渡して、再帰的な型解決を行います。

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

```typescript
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