この記事は エムスリー Advent Calendar 2017 の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

また、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のボイラープレートを生成します

$ 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

起動

$ ./bin/rails s
$ ./bin/webpack-dev-server

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

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

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

config/routes.rb
Rails.application.routes.draw do
  root to: 'users#index'
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
  end
end
app/views/layouts/application.html.erb
-    <%= stylesheet_link_tag    'application', media: 'all' %>
-    <%= javascript_include_tag 'application' %>
+    <%= javascript_pack_tag 'application' %>
app/views/users/index.html.erb
<div id="app"></div>
<%= javascript_pack_tag 'users/index' %>
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')
})
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に書き換えていきます。まずは依存ライブラリをインストール

$ 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の補完が効きません

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

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

ts-loaderの設定

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

webpacker3系の場合

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系の場合

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

typescriptに書き換える

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

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

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

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することで型推論されるようになります

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が補完候補に出できます !

スクリーンショット 2017-12-04 15.32.56.png

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

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の部分で補完が効いています!

スクリーンショット 2017-12-04 15.35.31.png

axiosを使う

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

$ yarn add axios

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

   root to: 'users#index'
+  get 'ajax/users', to: 'ajax/users#index'
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で取得するように変更

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のメソッドが補完されます

スクリーンショット 2017-12-04 14.42.26.png

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