Nuxt.js with TypeScript and Composition API

# はじめに
この記事は [Nuxt.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nuxt-js) 6日目の記事です。

皆さん、[Composition API](https://github.com/vuejs/composition-api) 使ってますか？ Composition API は現在開発中で、2020年1Qにもリリース予定の Vue.js 3.0 からデフォルトで搭載されることになっている新しいコンポーネントの実装方法です。Composition API を使うことで、散らかりがちだった実装を一箇所に固めることができたり、今まで組み合わせにくかった TypeScript との連携が比較的やりやすくなったりします。Vue.js 2.x にもプラグインとして導入すれば今すぐ使い始めることができるようになっています。

この記事では、その Composition API を Nuxt.js に導入し、さらに TypeScript を使ってなるべく[^1]型の力を借りて型安全なフロントエンドを作成する方法を紹介します。

# 環境構築
## 諸注意
今回は`npm`ではなく`yarn`を使っていきます。`npm`派の方は適宜コマンドを読み替えてください。また、コマンドは全てmacOS Catalinaでの実行例となっています。Linuxなどをお使いの方はパッケージマネージャーなどが出てきた際に、適切なコマンドに読み替えてください。

## 準備
とりあえず`node`と`yarn`を入れておきます。エディタはVSCodeやWebStormなどお好みで。

```bash
$ brew install node yarn
```

## プロジェクトの初期化
`yarn create` コマンドでざっくり作っていきます。`create-nuxt-app`ではまだTypeScriptを使ったプロジェクトの生成には対応していないので、いったんJavaScript用のプロジェクトを作ります。各種選択肢はお好みでOKです。一応どれを選んだかはここに書いておきます。

```shell-session
% yarn create nuxt-app composition-sample
yarn create v1.19.2
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-nuxt-app@2.12.0" with binaries:
      - create-nuxt-app

create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in composition-sample
? Project name composition-sample
? Project description My primo Nuxt.js project
? Author name Aruneko
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to i
nvert selection)
```

## TypeScript対応
基本的に[公式サイト](https://typescript.nuxtjs.org/)のやり方に従えばOKですが、簡単に手順を記しておきます。

まずはビルド時にTypeScriptを有効化する設定です。`@nuxt/typescript-build`を入れてから設定ファイルを書き換え、`tsconfig.json`を生成するところまでやっておきます。これはあくまでビルド時にしか使わないので、開発用依存パッケージとしてインストールします。

```bash
$ yarn add --dev @nuxt/typescript-build
```

インストールが終わったら`nuxt.config.js`を書き換えます。まず`buildModules`に`'@nuxt/typescript-build'`を書き加えましょう。

```javascript:nuxt.config.js
export default {
  // 前略
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxt/typescript-build',  // ここを追加
    '@nuxtjs/eslint-module',
    '@nuxtjs/vuetify'
  ],
  // 後略
}
```

そしてこの際なので`nuxt.config.js`を`nuxt.config.ts`にリネームして、型を付けます。`default export`しているあたりをいったん`nuxtConfig`変数に入れて型を付け、後からエクスポートするようにします。

```bash
$ mv nuxt.config.js nuxt.config.ts
```

```typescript:nuxt.config.ts
import { Configuration } from '@nuxt/types'
import colors from 'vuetify/es5/util/colors'

const nuxtConfig: Configuration = {
  // 中略
}

module.exports = nuxtConfig
```

最後に次のような内容で`tsconfig.json`をプロジェクトのトップ階層に設置すれば完了です。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}
```

続いてランタイム時にもTypeScriptが有効になるようにしていきます。これは`@nuxt/typescript-runtime`を導入して、`package.json`を書き換えるだけのお手軽作業です。こちらはランタイムなので、開発用依存としないように注意しましょう。

```bash
$ yarn add @nuxt/typescript-runtime
```

`package.json`の変更ですが、`scripts`セクションを探してその中にある4箇所の`nuxt`コマンドを全て`nuxt-ts`コマンドに置き換えるだけです。

```json:package.json
"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "start": "nuxt-ts start",
  "generate": "nuxt-ts generate",
  "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
}
```

最後にlintの設定もしましょう。せっかくESLintとPrettierをプロジェクト初期化時に有効にしてありますからね。まずは専用のESLint用定義を入れていきます。

```bash
$ yarn add -D @nuxtjs/eslint-config-typescript
```

インストールが終わったら`.eslintrc.js`でその定義を有効化しておきます。`parser` を TypeScript のものに変更し、`extends` セクションで導入した定義を有効化してください。

```javascript:.eslintrc.js
module.exports = {
  // 前略
  parserOptions: {
    // ここを変更
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended',
    '@nuxtjs/eslint-config-typescript'. // ここを追加
  ],
  // 後略
}
```

最後に`package.json`の`scripts`セクションにある`lint`コマンドで`.ts`ファイルに対してもlintが走るように対象となる拡張子を追加すれば完了です。

```json:package.json
"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "start": "nuxt-ts start",
  "generate": "nuxt-ts generate",
  "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."
}
```

## Composition API の導入
まずは yarn でインストールしてしまいましょう。

```bash
$ yarn add @vue/composition-api
```

インストールできたら plugins ディレクトリの下に `composition-api.ts` を作って、Composition API を有効化します。

```typescript:plugins/composition-api.ts
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)
```

`nuxt.config.ts` でプラグインを有効化することもお忘れなく。

```typescript:nuxt.config.ts
const nuxtConfig: Configuration = {
  // 前略
  plugins: ['@/plugins/composition-api'],
  // 後略
}
```

これで導入は完了です。

# Composition API による実装の例
## コンポーネントの作成
ではVuetifyを選択することによってデフォルトで生成されたコンポーネントを改造する形で、Composition API をどうやって Nuxt.js に組み込んでいくか説明していきます。

まずは `layouts/default.vue` を確認してみましょう。デフォルトの実装は以下のようになっています。なお下記コードでは`<script>`内を抜粋しています。

```javascript:layouts/default.vue
export default {
  data() {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
          to: '/'
        },
        {
          icon: 'mdi-chart-bubble',
          title: 'Inspire',
          to: '/inspire'
        }
      ],
      miniVariant: false,
      right: true,
      rightDrawer: false,
      title: 'Vuetify.js'
    }
  }
}
```

これを Composition API + TypeScript で書き換えていきます。まず `<script>` タグに `lang="ts"` 属性を追加して `<script lang="ts">` としてこの中に書かれたコードがTypeScriptであるということを明示してから、コードを書き換えていきます。まずは書き換えた後のコード全体を掲載しますが、おおよそは Composition API のサンプルコード通りとなります。

```vuejs:layouts/default.vue
<script lang="ts">
// (1) 必要なものをインポート
import { createComponent, ref } from '@vue/composition-api'

type Item = {
  icon: string
  title: string
  to: string
}

// (2) createComponentによるコンポーネントの作成
export default createComponent({
  setup() {
    // (3) refを使ったリアクティブ値の生成
    const clipped = ref(false)
    const drawer = ref(false)
    const fixed = ref(false)
    // (4) 型の明示
    const items = ref<Item[]>([
      {
        icon: 'mdi-apps',
        title: 'Welcome',
        to: '/'
      },
      {
        icon: 'mdi-chart-bubble',
        title: 'Inspire',
        to: '/inspire'
      }
    ])
    const miniVariant = ref(false)
    const right = ref(false)
    const rightDrawer = ref(false)
    const title = ref('Vuetify.js')

    // (5) Template内で使うものだけまとめて返す
    return {
      clipped,
      drawer,
      fixed,
      items,
      miniVariant,
      right,
      rightDrawer,
      title
    }
  }
})
</script>
```

### コンポーネントの作成
Composition API では、`(2)` で行っているように `createComponent` 関数によってコンポーネントを作成します。これは `@vue/composition-api` から提供されるので、`(1)` の箇所で事前にインポートしておきます。さらに `setup` を使ってリアクティブな値の定義を行っていきます。今までは `data()` を使ってこれらの値を定義していましたが、Composition API では変更されていますので注意してください。

### リアクティブ値
単体のリアクティブ値は `ref` を使って作成します。投入した初期値によって型推論が行われるため、`(3)` のように通常は型を明示する必要はありません。ただし、明示的に書きたい場合は `(4)` でやっているように `<>` を使えばOKです。

一方、型の明示が必須な場合もあります。例えば初期値では空配列だけれども後から値が変化するような場合や、`null` が入る可能性があるリアクティブ値を定義する場合には型を明示しなければなりません[^2]。例えば以下のようにすると良いでしょう。

```typescript
const userList = ref<User[]>([])
const nullableValue = ref<User | null>(null)
```

`setup` 関数の最後で `<template>` 内で使う値だけまとめて返してあげれば完了です。これで少なくとも `<script>` 内では TypeScript による型チェックが働くようになります[^3]。

## Nuxt固有の拡張機能
Nuxt.js には `asyncData` など便利機能が色々と備わっています。ですが、Composition API と組み合わせた場合これらの機能を使うことは現時点では非常に難しいです。そこで Composition API にある代替機能を使ってこれらの機能を実装していきましょう。ある程度はそれで機能の代わりを果たすことができます。

### 非同期値の取得
API からデータを引っ張ってくるなど非同期な値を持ってきたいことはしばしばあると思います。ただ先にも述べたように `asyncData` は封印されていますので、別の手段を用います。 Composition API には `watch` という関数が用意されており、これを使って `async` をラップしてあげることでページ遷移してきたときに1回だけ呼んであげることができます。本来 `watch` は名前の通りリアクティブ値を監視して、変更があったときに指定した関数を動かすための関数ですが、なぜか[^4]こういった使い方もできるようになっています。

```typescript
const users = ref<User[]>([])

watch(async () => {
    users.value = await fetch('http://api.example.com/users/')
})
```

### レイアウトやプロパティの指定
これは従来のやり方がそのまま使えます。`props` もOKです。`createComponent` の中でそれぞれに対応した Key-Value を設定してあげましょう。

```typescript
import { createComponent } from '@vue/composition-api'

export default createComponent({
  layout: 'empty',
  props: {
    user: {
      type: Object,
      default: null
    }
  },
  setup() {
    // 略
  }
})
```

### Nuxt Axios / Auth Moduleとの連携
これも問題なくできますが、少し準備が必要です。まず型付けを正しく行うために、`nuxt.config.ts` を編集します。なお、Auth Module を利用する際は事前にパッケージと型定義を導入しておいてください。あとは型定義をインポートして、`declare module` でどのプロパティにどの型を適用するか定義してあげましょう。

```bash
$ yarn add @nuxtjs/auth
$ yarn add -D @types/nuxtjs__auth
```

```typescript:nuxt.config.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import { Auth } from 'nuxtjs__auth'

const nuxtConfig: Configuration = {
  // 省略
}

// ここのひとかたまりを追加
declare module 'vue/types/vue' {
  interface Vue {
    $auth: Auth
    $axios: NuxtAxiosInstance
  }
}
```

使う側では `setup` メソッドの第2引数に渡されてくる `context` 引数に実装が詰め込まれているので、そこから読むようにします。`root` プロパティの中に先ほど `interface Vue` で指定したプロパティが生えているので、呼んであげるだけです。補完もバッチリ効きますので、便利に使うことができます。

```typescript
export default createComponent({
  setup(_props, context) {
    const users = ref<User[]>([])
    watch(async () => {
      // Axios Module を呼ぶ例
      users.value = await context.root.$axios.$get('/users')
    })

    const login = async () => {
      // Auth Module を呼ぶ例
      await context.root.$auth.loginWith(/* ユーザー名とパスワードを送信 */)
      context.root.$router.push('/')
    }

    return { users, login }
  }
})
```

`context.root.$router.push` なんかをしれっと使ってますが、だいたい欲しいもの（`$el` や `$store` など）は `context.root` に生えているので、困ったらまずここを探してみると良いでしょう。何が入っているかは補完機能が全部教えてくれます。ちなみに `emit` は `context.emit` と `context` 直下にぶら下がっています。

# おわりに
ここまで Nuxt.js に Composition API を導入し、TypeScript で型を付けながら実装する方法を紹介してきました。Composition API 自体がまだちょっと洗練されておらず、特に Nuxt.js が用意している専用機能に関するサポートに関しては手つかずの状況ではあります。しかし、あんまり凝ったことをしないのであれば十分使えるかなといった手応えです。来年Q1にも予定されている Vue.js 3.0 とそれをベースにした新しい Nuxt.js を楽しみにしつつ、年を越したいと思います。

[^1]: たまに`any`を書かざるを得ないのはご愛敬
[^2]: 例えば初期値に `null` を入れたら `Ref<null>` に推論されるので、後から何かしらの型を持つ値を代入しようとしたときに型エラーになります
[^3]: `<template>` 内で型チェックできる方法があれば教えてください......
[^4]: 初回に計算されて、それ以降変更を検知する箇所がないため、そういう動作になるものと思われます