Rails Vue.js TypeScriptでthisの型推論

この記事は [エムスリー Advent Calendar 2017](https://qiita.com/advent-calendar/2017/m3) の4日目の記事です。

Vue.js 2.5でTypeScript対応が強化されました。2.4以前は、Vueのデフォルトであるオブジェクトリテラルベースの構文でthisの型を推論することができませんでした（別途`vue-class-component`などを使いクラスベースの構文で書く必要がありました）。しかし、Vue.js 2.5でTypeScript対応が強化され、オブジェクトリテラルベースの構文でもthisの推論がきくようになりました。see: https://jp.vuejs.org/2017/09/23/upcoming-typeScript-changes-in-vue-2.5/


例（エディタはVSCodeでVeturプラグインを使用）

![vue-typescript_01.gif](https://qiita-image-store.s3.amazonaws.com/0/4506/14f44b0a-9374-d290-1cab-45c89552db6f.gif)

また、Rails 5.1でwebpackerが統合され、webpackを使いやすくなりました。この記事では、Rails5.1（webpacker）でVue.js 2.5（with TypeScript）を使える状態するまでの手順を書きます。なお、各verは以下の通り。エディタはVSCodeを使用します（Veturプラグインを入れてます）

```
# 以下、検証環境
rails: 5.1.4
webpacker: 3.0.2 
vue: 2.5.9
typescript: 2.6.2
VSCode: 1.18.1
Vetur: 0.11.3
```

## node, yarnのインストール

webpackerはnodeとyarnが必須なので、入れてない場合は入れます。see: https://github.com/rails/webpacker

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

## プロジェクト作成

railsプロジェクトを新規作成します。webpackerを有効にしてrails newし、vueのボイラープレートを生成します

```bash
$ rails new アプリの名前 --webpack --skip-turbolinks
$ cd アプリの名前
$ echo '2.4.2' > .ruby-version
$ echo "/vendor/bundle" >> ./.gitignore
$ bundle --path vendor/bundle --jobs=4
$ bundle exec rails webpacker:install:vue
```

起動

```bash
$ ./bin/rails s
```

```bash
$ ./bin/webpack-dev-server
```

http://localhost:3000 にアクセス

## ページを作成し、Vue.jsを動かす

まずはTypeScriptを使わずに、Vue.jsを普通のjavascriptで使ってみましょう。トップページにアクセスすると、「ユーザー一覧」と表示されるページを作ってみます

```ruby:config/routes.rb
Rails.application.routes.draw do
  root to: 'users#index'
end
```

```ruby:app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
  end
end
```

```diff:app/views/layouts/application.html.erb
-    <%= stylesheet_link_tag    'application', media: 'all' %>
-    <%= javascript_include_tag 'application' %>
+    <%= javascript_pack_tag 'application' %>
```

```erb:app/views/users/index.html.erb
<div id="app"></div>
<%= javascript_pack_tag 'users/index' %>
```

```javascript:app/javascript/packs/users/index.js
import Vue from 'vue'
import UserIndex from '../../src/components/UserIndex.vue'

document.addEventListener('DOMContentLoaded', () => {
  new Vue(UserIndex).$mount('#app')
})
```

```vue:app/javascript/src/components/UserIndex.vue
<template>
    <div>
        <h1>{{ title }}</h1>
    </div>
</template>

<script>
export default {
    data() {
        return {
            title: "ユーザー覧"
        }
    }
}
</script>
```

localhost:3000にアクセスすると、画面に「ユーザー一覧」と表示され、コンソールには`Hello World from Webpacker`が表示されればOKです。（Hello Worldの文は、`app/javascript/packs/application.js`が出力しています）


## TypeScriptを使う

では、上記のscriptの部分をTypeScriptに書き換えていきます。まずは依存ライブラリをインストール

```bash
$ yarn add typescript ts-loader
```

## tsconfig.jsonを追加

TypeScriptの設定ファイルであるtsconfis.jsonをトップ階層に追加します。Vue.jsの公式ドキュメントにtsconfig.jsonの推奨構成が書かれているので、これを参考にします: https://vuejs.org/v2/guide/typescript.html#Recommended-Configuration


ポイントは、`noImplicitThis`。これを有効にしないとthisの補完が効きません

```json:tsconfig.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es6", "dom"],
    "module": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5",
    // strict: trueにすると、以下のオプションが全部trueになる
    // --noImplicitAny, --noImplicitThis, --alwaysStrict and --strictNullChecks.
    "strict": true
  },
  "exclude": [
    "**/*.spec.ts",
    "node_modules",
    "vendor",
    "public"
  ],
  "compileOnSave": false
}
```


## .vueの型定義を追加

see: https://github.com/Microsoft/TypeScript-Vue-Starter#single-file-components

```typescript:app/javascript/src/types/vue-shims.d.ts
declare module "*.vue" {
    import Vue from 'vue'
    export default Vue
}
```

## ts-loaderの設定

webpacker3系と2系で設定ファイルの構成が異なります

webpacker3系の場合

```js:config/webpack/environment.js
const { environment } = require('@rails/webpacker')

environment.loaders.set('typescript', {
  test: /\.ts$/,
  loader: 'ts-loader',
  options: {
    appendTsSuffixTo: [/\.vue$/]
  }
})

module.exports = environment
```

参考: webpacker2系の場合

```js:config/webpack/loaders/typescript.js
module.exports = {
  test: /\.ts$/,
  loader: 'ts-loader',
  options: {
    appendTsSuffixTo: [/\.vue$/]
  }
}
```


## typescriptに書き換える

エントリポイントになるpacks配下の拡張子をjsからtsに変更します

```shell
$ mv app/javascript/packs/users/index.js app/javascript/packs/users/index.ts
```

scriptの部分にlang="ts"を追加し、コードをTypeScriptに書き換えます

```diff:app/javascript/src/components/UserIndex.vue
 <template>
     <div>
         <h1>{{ title }}</h1>
+        <p>{{ description }}</p>
     </div>
 </template>

-<script>
+<script lang="ts">
 export default {
     data() {
         return {
             title: "ユーザー覧"
         }
+    },
+    computed: {
+        description(): string {
+            return "ユーザー一覧ページです"
+        }
     }
 }
 </script>
```

scriptの部分がtypescriptとして評価されるようになりました！

## thisを型推論

ただ、このままではthisの型推論がされません。Vue.extendすることで型推論されるようになります

```diff:app/javascript/src/components/UserIndex.vue
 <script lang="ts">
-export default {
+import Vue from 'vue'
+
+export default Vue.extend({
     data() {
         return {
             title: "ユーザー覧"
@@ -14,8 +16,8 @@ export default {
     },
     computed: {
         description(): string {
-            return "ユーザー一覧ページです"
+            return `${this.title}ページです`
         }
     }
-}
+})
```

上記でthis.tとタイプすると、titleが補完候補に出できます ！

<img width="717" alt="スクリーンショット 2017-12-04 15.32.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/4506/4c8edd34-77d2-2a21-42ee-a34e5e9bbe4d.png">


もう少しコードを追加して、thisの型推論の嬉しみを感じてみましょう。独自のinterfaceであるUserを追加して、リスト操作してみます。

```diff:app/javascript/src/components/UserIndex.vue
     <div>
         <h1>{{ title }}</h1>
         <p>{{ description }}</p>
+
+        <h2>管理者ユーザー</h2>
+        <ul>
+            <li v-for="adminUser in adminUsers" :key="adminUser.id">
+                {{ adminUser.name }}
+            </li>
+        </ul>
+
+        <h2>一般ユーザー</h2>
+        <ul>
+            <li v-for="generalUser in generalUsers" :key="generalUser.id">
+                {{ generalUser.name }}
+            </li>
+        </ul>
     </div>
 </template>

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

+interface User {
+    id: number
+    name: string
+    age: number
+    is_admin: boolean
+}
+
 export default Vue.extend({
     data() {
         return {
-            title: "ユーザー覧"
+            title: "ユーザー覧",
+            users: [
+                { id: 1, name: "hoge", age: 30, is_admin: true},
+                { id: 2, name: "fuge", age: 27, is_admin: false},
+                { id: 3, name: "foo", age: 22, is_admin: false},
+            ] as User[]
         }
     },
     computed: {
         description(): string {
             return `${this.title}ページです`
+        },
+        adminUsers(): User[] {
+            return this.users.filter(user => user.is_admin)
+        },
+        generalUsers(): User[] {
+            return this.users.filter(user => !user.is_admin)
         }
     }
 })
```

computedの中でusers.filterやuser.is_adminの部分で補完が効いています！

<img width="765" alt="スクリーンショット 2017-12-04 15.35.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/4506/36e171c8-2486-1735-8324-e9cfe4e5c43f.png">


## axiosを使う

最後にusersの部分をajaxでサーバーから取得するように変更してみましょう。httpリクエストにはaxiosを使うことにします

```
$ yarn add axios
```

ajaxでユーザー一覧を取得するendpointを作成します


```diff
   root to: 'users#index'
+  get 'ajax/users', to: 'ajax/users#index'
```

```ruby:app/controllers/ajax/users_controller.rb
class Ajax::UsersController < ApplicationController
  def index
    users = [
      FakeUser.new(1, "hoge", 30, true),
      FakeUser.new(2, "fuge", 27, false),
      FakeUser.new(3, "foo", 22, false),
    ]

    render json: users
  end

  class FakeUser < Struct.new(:id, :name, :age, :is_admin); end
end
```

usersをajaxで取得するように変更

```diff:app/javascript/src/components/UserIndex.vue
     data() {
         return {
             title: "ユーザー覧",
-            users: [
-                { id: 1, name: "hoge", age: 30, is_admin: true},
-                { id: 2, name: "fuge", age: 27, is_admin: false},
-                { id: 3, name: "foo", age: 22, is_admin: false},
-            ] as User[]
+            users: [] as User[]
+        }
+    },
+    async created() {
+        try {
+            const res = await axios.get("/ajax/users")
+            this.users = res.data
+        } catch (e) {
+            alert(e)
         }
     },
```

axiosには型定義が含まれているので、`import axios from 'axios'`するだけで、こんな感じでaxiosのresposeのメソッドが補完されます

<img width="878" alt="スクリーンショット 2017-12-04 14.42.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/4506/d2927257-faba-ab07-93ad-b8fb00b8dc15.png">


このようにVue.js 2.5以降ではTypeScriptを導入しやすくなっていています！現時点ではtemplateの部分の補完は効かないようですが、今後templateでも補完が効くようになれば益々TypeScript導入のメリットが増えてきますね！