create-nuxt-appで構築したNuxt2のプロジェクトにTypeScriptを入れてvuexを使えるところまで持っていく

# はじめに

Nuxt1を軽く触った程度なのと、tsほぼ未経験の状態でこの記事を記載していますので間違っているところはぜひご指摘頂けますと幸いです🙇

# 前提

- nuxt
    - 2.0.0
- typescript
    - 3.1.1

# やってみる

## Nuxt2をインストールする

執筆時点ではver2インストールの和訳はされていないので[enページ](https://ja.nuxtjs.org/guide/installation/)の手順通り行います。
npxを使ったことがなかったので今回はnpxでやってみます。

```shell
npx create-nuxt-app sample-project
```

色々聞かれますが、今回は以下の通りにしてみました。
<img width="617" alt="スクリーンショット 2018-10-02 18.45.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/173370/233cbdb7-daa0-3a77-704a-dca2c37c44d6.png">


項目 | 選択肢 | 補足
--- | --- | ---
Project name | 適当 |
Project description | 適当 |
Use a custom server framework | none | 素のやつ以外使ったことない😇
Use a custom UI framework | vuetify | vuesax使ってたけどこの選択肢の中だったらなんとなくこれかな、くらいの気持ち
Choose rendering mode | Single Page App | SPAしかやったことない😇し、SSRも必要なかった
Use axios module | yes |
Use eslint | no | TypeScript使うので
Use prettier | no | TypeScript使うので
Author name | 適当 |
Choose a package manager | yarn |

無事インストール完了しました。

<img width="687" alt="スクリーンショット 2018-10-02 18.45.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/173370/85a9d419-4a1e-d417-69a5-bce0de536e4f.png">

## TypeScriptを入れる

### パッケージ導入

まずはtypescriptとtsの関連パッケージを入れます。

```shell
yarn add typescript ts-loader tslint tslint-config-prettier --dev
```

今回はデコレータを使いたかったのでデコレータも入れます。

```shell
yarn add nuxt-property-decorator --save
```

### 関連ファイル追加

次に、.tsを読み込むための定義ファイルを作成します。

ファイルの内容は[nuxt-communityのテンプレ](https://github.com/nuxt-community/typescript-template/blob/master/template/modules/typescript.js)をそのまま使いまわします。

```javascript:modules/typescript.js
module.exports = function() {
  // Add .ts extension for store, middleware and more
  this.nuxt.options.extensions.push("ts")
  // Extend build
  this.extendBuild(config => {
    const tsLoader = {
      loader: "ts-loader",
      options: {
        appendTsSuffixTo: [/\.vue$/]
      }
    }
    // Add TypeScript loader
    config.module.rules.push(
      Object.assign(
        {
          test: /((client|server)\.js)|(\.tsx?)$/
        },
        tsLoader
      )
    )
    // Add TypeScript loader for vue files
    for (let rule of config.module.rules) {
      if (rule.loader === "vue-loader") {
        rule.options.loaders = {
          ts: "tsLoader",
        }
      }
    }
    // Add .ts extension in webpack resolve
    if (config.resolve.extensions.indexOf(".ts") === -1) {
      config.resolve.extensions.push(".ts")
    }
  })
}
```

typescript.jsをnuxt.config.jsで読み込むようにします。

```javascript:nuxt.config.js
  modules: [
    '@nuxtjs/axios',
    '~/modules/typescript.js' // この行を追加
  ],
```


次に、.vueを型定義するためのindex.d.tsを作成します。

ファイルの内容はmodules/typescript.js同様[nuxt-communityのテンプレ](https://github.com/nuxt-community/typescript-template/blob/master/template/index.d.ts)をそのまま使いまわします。

```typescript:index.d.ts
declare module "*.vue" {
  import Vue from "vue"
  const _default: Vue
  export default _default
}
```

続いて、tsコンパイルの指定をする為にtsconfig.jsonを作成します。

こちらも[nuxt-communityのテンプレ](https://github.com/nuxt-community/typescript-template/blob/master/template/tsconfig.json)をそのまま使いまわします。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "es2015"],
    "module": "es2015",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "noImplicitThis": false,
    "strictNullChecks": true,
    "removeComments": true,
    "suppressImplicitAnyIndexErrors": true,
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./*"]
    }
  }
}
```

tslintも入れたので設定を入れちゃいます。
これはnuxt-communityにないのでひとまず自作のものにしておきます。

```json:tslint.json
{
  "defaultSeverity": "error",
  "extends": [
    "tslint:recommended",
    "tslint-config-prettier"
  ],
  "jsRules": {},
  "rules": {
    "no-namespace": false,
    "no-string-literal": false,
    "max-line-length": {
      "options": {"limit": 120},
      "severity": "warn"
    },
    "interface-name": [true,  "never-prefix"],
    "ordered-imports": false,
    "object-literal-sort-keys": false
  },
  "rulesDirectory": []
}
```

### js→tsに書き換えてみる

これでtypescriptが使えるようになったので、テンプレのlayouts/default.vueファイルにtypescriptを組み込んでみます。

変更前はこちら

```vue:layouts/default.vue
// /////////
// before
// /////////
// templateは省略
<script>
  export default {
    data() {
      return {
        clipped: false,
        drawer: true,
        fixed: false,
        items: [
          { icon: 'apps', title: 'Welcome', to: '/' },
          { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' }
        ],
        miniVariant: false,
        right: true,
        rightDrawer: false,
        title: 'Vuetify.js'
      }
    }
  }
</script>
```

変更後はこちら

```vue:layouts/default.vue
// /////////
// after
// /////////
// templateは省略
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class extends Vue {
  clipped = false
  drawer = true
  fixed = false
  items = [
    { icon: 'apps', title: 'Welcome', to: '/' },
    { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' }
  ]
  miniVariant = false
  right = true
  rightDrawer = false
  title = 'Vuetify.js'
}
</script>
```

### 余談

ちょっと冗長ですが、現時点では.vueをtsにする場合、

```vue:xxx.vue
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class extends Vue {}
</script>
```

こいつを決め打ちで入れてやる必要があるのかなと思います。

デコレータの書き方は[README](https://github.com/nuxt-community/nuxt-property-decorator)を見ればわかるかなと思います。
(data()の書き方がわからなくてちょっと焦りました😅)

## vuexのテンプレをつくる

TS初心者にはこれが結構きつかったです😅

調べた(ザッピング程度ですが)ところ、[こちらの記事](https://codeburst.io/vuex-and-typescript-3427ba78cfa8)がすごく親切だったので参考にさせて頂きました。
※そのままだとtslintで怒られたりうまく動かないところがあったのでところどころ修正しています

### やっていることの概要

導入方法の前にやっていることをざっくり説明します。

やっていることは、

- vuex共通の型定義をする
- versionというプロパティを作成する(stateだけのモデル)
- profileというモデルを作成する

こんな感じです。

### パッケージの導入

まずはライブラリを入れます。

```shell
yarn add vuex-class --save
```

### 関連ファイル追加

次に、vuexで最終的に全てのモデルをまるっと入れておくための型を定義します。

```typescript:store/types.ts
export interface RootState {
  version: string
}
```

続いて、共通の型定義ファイルを作成します。

```typescript:store/vuex/types/index.d.ts
import {Action, Mutation, Getter} from 'vuex'

export interface ActionTree<S, R> {
  [key: string]: Action<S, R>
}
export interface MutationTree<S> {
  [key: string]: Mutation<S>
}
export interface GetterTree<S, R> {
  [key: string]: Getter<S, R>
}
export interface Module<S, R> {
  namespaced?: boolean
  state?: S | (() => S)
  getters?: GetterTree<S, R>
  actions?: ActionTree<S, R>
  mutations?: MutationTree<S>
}
```

次に、ベースとなるindex.tsを作成します。

```typescript:store/index.ts
import Vue from 'vue'
import Vuex, { StoreOptions } from 'vuex'
import { RootState } from '~/store/types'
import { profile } from './profile/index'

Vue.use(Vuex)

const store: StoreOptions<RootState> = {
  modules: {
    profile
  },
  state: {
    version: '1.0.0' // a simple property
  }
}

const createStore = () => {
  return new Vuex.Store<RootState>(store)
}

export default createStore
```

最後に、profileモデルの為に必要なファイルを作成します。

```typescript:store/profile/index.ts
import { Module } from 'vuex'
import { getters } from './getters'
import { actions } from './actions'
import { mutations } from './mutations'
import { ProfileState } from './types'
import { RootState } from '../types'

export const state: ProfileState = {
  user: undefined,
  error: false
}

const namespaced: boolean = true

export const profile: Module<ProfileState, RootState> = {
  namespaced,
  state,
  getters,
  actions,
  mutations
}
```

```typescript:store/profile/actions.ts
import { ActionTree } from 'vuex'
// APIは別の話なので今回は固定値としておきます
// import axios from 'axios'
import { ProfileState, User } from './types'
import { RootState } from '../types'

export const actions: ActionTree<ProfileState, RootState> = {
  fetchData({ commit }): any {
    // APIは別の話なので今回は固定値としておきます
    // axios({
    //   url: 'https://....'
    // }).then((response) => {
    //   const payload: User = response && response.data
    //   commit('profileLoaded', payload)
    // }, (error) => {
    //   console.log(error)
    //   commit('profileError')
    // })
    const payload = {
      firstName: 'firstNameのデータ',
      lastName: 'lastNameのデータ',
      email: 'sample@example.com',
      phone: '＋819000000000'
    }
    commit('profileLoaded', payload)
  }
}
```

```typescript:store/profile/getters.ts
import { GetterTree } from 'vuex'
import { ProfileState } from './types'
import { RootState } from '../types'

export const getters: GetterTree<ProfileState, RootState> = {
  fullName(state): string {
    const { user } = state
    const firstName = (user && user.firstName) || ''
    const lastName = (user && user.lastName) || ''
    return `${firstName} ${lastName}`
  }
}
```

```typescript:store/profile/mutations.ts
import { MutationTree } from 'vuex'
import { ProfileState, User } from './types'

export const mutations: MutationTree<ProfileState> = {
  profileLoaded(state, payload: User) {
    state.error = false
    state.user = payload
  },
  profileError(state) {
    state.error = true
    state.user = undefined
  }
}
```

```typescript:store/profile/types.ts
export interface User {
  firstName: string
  lastName: string
  email: string
  phone?: string
}

export interface ProfileState {
  user?: User
  error: boolean
}
```

### 確認してみる

vuex側のテンプレは以上です。
vuexからデータを引っ張ってくるサンプルページを作成します。

```vue:pages/sample.vue
<template>
  <div class="container">
    <p>
      version: {{ version }}
    </p>
    <div v-if="profile.user">
      <p>
        Full name: {{ fullName }}
      </p>
      <p>
        Email: {{ email }}
      </p>
    </div>
    <div v-if="profile.error">
      Oops an error occured
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { State, Action, Getter } from 'vuex-class'
import Component from 'vue-class-component'
import { ProfileState, User } from '~/store/profile/types'
const namespace: string = 'profile'
@Component
export default class UserDetail extends Vue {
  @State('version') version: string
  @State('profile') profile: ProfileState
  @Action('fetchData', { namespace }) fetchData: any
  @Getter('fullName', { namespace }) fullName: string

  mounted() {
    // fetching data as soon as the component's been mounted
    this.fetchData()
  }

  // computed variable based on user's email
  get email() {
    const user = this.profile && this.profile.user
    return (user && user.email) || ''
  }
}
</script>
```

作成したページのメニューを追加します。

```vue:layouts/default.vue
// templateは省略
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class extends Vue {
  clipped = false
  drawer = true
  fixed = false
  items = [
    { icon: 'apps', title: 'Welcome', to: '/' },
    { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' },
    { icon: 'account_box', title: 'Sample', to: '/sample' } // ここを追加
  ]
  miniVariant = false
  right = true
  rightDrawer = false
  title = 'Vuetify.js'
}
</script>
```

長くなりましたがようやく完成です！
確認してみます！

![スクリーンショット 2018-10-03 15.19.46.png](https://qiita-image-store.s3.amazonaws.com/0/173370/b7962003-9ef8-6d1d-1d62-0362dde6c759.png)

無事にデータが表示出来ました！！！

## 蛇足(scssを使えるようにする)

タイトルと関係ないですね😅
が、scssも結構入れる人多いと思うので記載しておきます。

必要なパッケージを導入します。

```shell
yarn add node-sass sass-loader --dev
```

まずは.vueファイルに記載する場合、

```vue:xxx.vue
// template, scriptは省略
<style lang="scss" scoped>
.sample {
  .text {
    color: blue;
  }
}
</style>
```

これだけです。

次に、共通ファイルを読み込む場合、

```scss:assets/sass/mixins.scss
.sample {
  .text {
    background-color: red;
  }
}
```

ファイルを作成して、

```javascript:nuxt.config.js
  css: [
    '~/assets/style/app.styl',
    '~/assets/sass/mixins.scss' // ここに追加
  ],
```

これだけですね。
~~ちょっとここ定かではないんですが、Nuxt2になってsass-resource-loaderなしで行けるようになったんですかね？~~
外部のscssファイルを読み込むだけなら大丈夫ですが、その中で変数を使う場合はnuxt-sass-resource-loaderが必要でした。
[こちらの記事](https://qiita.com/uto-usui/items/6b745203fa6fad577877#nuxt-sass-resources-loader-%E3%81%A7-%E3%81%8B%E3%82%93%E3%81%9F%E3%82%93%E3%81%AB%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80)ですごく丁寧に書かれていますね。


■追記■
デフォでstylus入ってたので無理にscssを使えるようにする必要もなかったですね😅

# さいごに

vuexの部分、今回は参考にさせて頂いた記事を真似てactions, getters, mutations, typesをファイル分割しましたが、よほどでかくならない限り個人的には1ファイルでも良いのかなぁと思ったりしました。

サンプル通りに設定してみたつもりだけどうまく動かない😇、みたいなことが度々あったのでもう少し中身を理解しないといけないよなと痛感しました(当たり前

Vue3系もTypeScriptで書き直されるとのことですし、もうTypeScript化の流れから逃げることは出来ないと思うので、私と同じくTypeScriptそろそろさわらなきゃなぁと思っている方の一助になれば幸いです🙇
冒頭に触れましたが、間違っているところはぜひご指摘頂けますと幸いです:bow: