Nuxt.js + apollo-module(vue-apollo) + TypeScriptで型をつける方法

Vue.jsでGraphQLを使う際は、[Vue Apollo](https://vue-apollo.netlify.com/)が選択肢の一つになると思います。

GraphQLのクライアントはfetchやXMLHttpRequestを使用しても実装できます。しかし、Nuxt.jsでSSRすることを考えると、サーバサイドでも取得できるようにしたり、Vueのdataとマージしたりと、色々と考慮すべきことや実装すべきポイントがあります。Vue Apolloを使うと、こういった部分であまり悩まなくて済みます。

※ただし、Apolloは高機能である分、使いこなすまで時間がかかりますし、ユースケースによってはオーバースペックな可能性もあります。

Vue ApolloはVue.jsのプラグインで、[ApolloのGraphQLクライアント](https://github.com/apollographql/apollo-client)をVue.jsに統合することができます。

さらに、Nuxt.jsの[apollo-module](https://github.com/nuxt-community/apollo-module)は、Vue ApolloをNuxt.jsに統合しています。

この記事では、Nuxt.js + TypeScript + apollo-moduleで開発する際に、型をつける方法を解説します。

なお、この記事で使っているソフトウェアのバージョンは下記の通りです。

- nuxt: 2.12.0
    - vue: 2.6.11
- typescript: 3.8.3
- nuxtjs/apollo: 4.0.0-rc19
    - vue-apollo: 3.0.3

また、本記事のサンプルコードは下記リポジトリでも公開しています。

https://github.com/ryo-utsunomiya/nuxt-apollo-typescript

## セットアップ

下記コマンドでNuxtプロジェクトを作成します。

```bash
npx create-nuxt-app nuxt-apollo-typescript
```

セットアップ中の設定は、プログラミング言語がTypeScript、レンダリングモードがUniversal (SSR)になっていれば、他は何でもよいです。

## fetchによるGraphQLクライアント

まずはシンプルなところから始めたいので、fetchを使った最小限のGraphQLクライアントを作ってみます。GraphQL APIはサンプルとしてよく使われる[SW API](https://swapi-graphql.netlify.com)を使います。

```pages/index.vue
<template>
  <div :class="$style.container">
    <h1>Star wars films</h1>
    <ul>
      <li v-for="film in films" :key="film.episodeID">
        {{ film.title }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

// GraphQLのレスポンスに型をつける
interface Film {
  episodeID: number
  title: string
}
interface Edge {
  node: Film
}
interface FilmConnection {
  edges: Edge[]
}
interface ResponseData {
  allFilms: FilmConnection
}
interface Response {
  data: ResponseData
}

// Vueのdataに型をつける
interface VueData {
  films: Film[]
}

export default Vue.extend({
  data(): VueData {
    return {
      films: []
    }
  },
  async created() {
    const query = `
{
  allFilms(first: 3) {
    edges {
      node {
        episodeID
        title
      }
    }
  }
}
`
    const res = await fetch(
      'https://swapi-graphql.netlify.com/.netlify/functions/index',
      {
        method: 'POST',
        body: JSON.stringify({
          query
        }),
        headers: {
          'Content-Type': 'application/json'
        }
      }
    ).then<Response>((res) => res.json())

    this.films = res.data.allFilms.edges.map((e) => e.node)
  }
})
</script>

<style module>
.container {
  margin: 10px;
}
</style>
```

上手くいったら、↓のようにタイトルのリストが表示されます。

<img width="232" alt="Screen Shot 2020-03-25 at 1.45.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33450/c0de65ba-deef-fc12-f1a2-f04e98066525.png">

ただし、このコードには残念な点もあります。 `fetch` はブラウザのAPIなので、SSR時にデータを取得しようとするとエラーになります（ `created` => `asyncData` に変更すると、SSR時のエラーを再現できます）。

この問題の解決策としては [node-fetch](https://www.npmjs.com/package/node-fetch) を使うといった方針もありですが、ここではNuxtのapollo-moduleを使って解決してみます。

## apollo-moduleのインストール

まず、[公式のインストールガイド](https://github.com/nuxt-community/apollo-module#setup)に沿って、apollo-moduleをインストールします。

```bash
yarn add @nuxtjs/apollo graphql-tag
```

nuxtの設定ファイルにモジュールを読み込み、最低限の設定を記述します。

```nuxt.config.js
  modules: ['@nuxtjs/apollo'],
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint:
          'https://swapi-graphql.netlify.com/.netlify/functions/index'
      }
    }
  },
```

この状態で、pages/index.vueのscriptを以下のように書き換えると、Apolloを使ってデータが取得できるようになります！

```pages/indev.vue
<script>
import gql from 'graphql-tag'

export default {
  computed: {
    films() {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
}
</script>
```

Vueコンポーネントの定義にapolloというプロパティを生やして、ここにGraphQL APIに対するクエリを書くと、レスポンスがdataにマージされます。

## TypeScriptの型定義

現状では、apolloによるデータ取得はできましたが、TypeScriptの型は付いていません。そこで、 `lang="ts"` にして、 `Vue.extend()` を使って型をつけようとすると、コンパイルエラーが発生します。

```pages/index.vue
<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'

export default Vue.extend({
  computed: {
    films() {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
})
</script>
```

コンパイルエラー：

```
No overload matches this call.
  The last overload gave the following error.
    Argument of type '{ computed: { films(): any; }; apollo: { allFilms: DocumentNode; }; }' is not assignable to parameter of type 'ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<...>>'.
      Object literal may only specify known properties, and 'apollo' does not exist in type 'ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<...>>'.
```

これは、VueのOptions APIにapolloというフィールドが存在しないために発生しています。そこで、tsconfig.jsonを修正し、VueのOptions APIでapolloが定義できるよう修正しましょう。

```diff
  "compilerOptions": {
    "types": [
      "@types/node",
      "@nuxt/types",
+     "vue-apollo"
    ]
  }
```

これで、TypeScriptのコンパイル時に[Vue Apolloが定義しているapolloの型定義](https://github.com/vuejs/vue-apollo/tree/dev/packages/vue-apollo/types)が読み込まれるため、apolloの型がつくようになります。

あとは `this.allFilms` の型を直せば、コンパイルが通るようになります。

```pages/index.vue
<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'

// GraphQLのレスポンスに型をつける
interface Film {
  episodeID: number
  title: string
}
interface Edge {
  node: Film
}
interface FilmConnection {
  edges: Edge[]
}

// Vueのdataに型をつける
interface Data {
  allFilms: FilmConnection
}

export default Vue.extend({
  data(): Data {
    return {
      allFilms: {
        edges: []
      }
    }
  },
  computed: {
    films(): Film[] {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
...
```

## レスポンスの型をスキーマから生成する

現状では、 `FilmConnection` のような型を手書きしていますが、こういった型はGraphQLのスキーマから自動生成したいところ。そこで、まずは以下のURLからスキーマをもってきます。

https://github.com/graphql/swapi-graphql/blob/master/schema.graphql

これを `schema.graphql` といった名前でプロジェクト内に置いておきます。次に、[GraphQL Code Generator](https://graphql-code-generator.com/)を使って、このスキーマからTypeScriptの型を生成します。

まずはライブラリのインストール

```bash
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript
```

次に設定ファイル。

```codegen.yml
overwrite: true
schema: "schema.graphql"
generates:
  lib/GraphQL/generated.ts:
    plugins:
      - "typescript"
```

準備ができたらコマンドを叩きます。

```bash
yarn run graphql-codegen --config codegen.yml
```

上手くいけば、以下のようなファイルがができているはず。

```lib/GraphQL/generated.ts
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};
...
```

あとは、これをimportして使うだけです。先ほどと少し型定義が変わってるので、それに合わせてコードも直してます。

```pages/index.vue
<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'
import { FilmsConnection, Film } from '~/lib/GraphQL/generated'

interface Data {
  allFilms?: FilmsConnection
}

export default Vue.extend({
  data(): Data {
    return {
      allFilms: undefined
    }
  },
  computed: {
    films(): Film[] {
      if (this.allFilms == null || this.allFilms.edges == null) return []
      return this.allFilms.edges
        .map((e) => e?.node)
        .filter((f): f is Film => f != null)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
})
</script>
```

これによって、 `lang="ts"` なVueコンポーネントでapolloが使えるようになり、さらに、GraphQLのレスポンスに型がつけられるようになりました。