Vue.jsとTypeScript

# はじめに
[この記事](http://qiita.com/kaorun343/items/a20f56d2bf519dc0610b)を書いて１年経ちました。
１年間でVue.jsとTypeScriptの環境は大きく変わりました。まずは、その辺を色々と紹介していきます。そのあと、サンプルを作成します。

# 2017年3月19日に追記
作成したサンプルがエラー吐くとのissueを受けたのを機に、サンプルを結構いじりました。サンプルも必ずチェックしてね。

# この１年間で変わったこと

## Vue.js 2.0がリリースされた
新しいメジャーリリースに際し、TypeScript関連で一番大きく変わったのは、型定義ファイルの提供方法です。バージョン１まではDefinitelyTypedに型定義ファイルを登録していました。そのため、tsdやtypingsで型定義ファイルをダウンロードする必要がありました。

一方新しいバージョンからは、[Vue.jsのリポジトリ内に型定義ファイルがあります](https://jp.vuejs.org/v2/guide/comparison.html#TypeScript)。したがって、npmなどを使ってライブラリをダウンロードするだけで使えるようになります。Vue.js本体だけでなく、いくつかの公式プラグインにも提供されています。

現在のところ、型定義ファイルがバンドルされているパッケージは以下の通りです。

* [vue](https://github.com/vuejs/vue/tree/dev/types)
* [vuex](https://github.com/vuejs/vuex/tree/dev/types)
* [vue-router](https://github.com/vuejs/vue-router/tree/dev/types)
* [vue-class-component](https://github.com/vuejs/vue-class-component/tree/master/src) （ソースコードがTypeScriptで記述されています）

## TypeScript 2.0がリリースされた
TS 2.0で色々と大きな変更が入りました。詳しくはこちらを参考にしてください（[TypeScript 2.0 Beta 変更点 - Qiita](http://qiita.com/vvakame/items/ae239f3d6f6f08f7c719)）。Vue.js向け型定義ファイルを作成する上で、嬉しかった変更は、`function` 内で `this` を明示できるようになったことです。

Vue.jsでコンポーネントを定義するとき、 `ComponentOptions` の中にプロパティやメソッド、ライフサイクルフックを記述します。

```ts
export default {
  data () {
    return {
      key: ''
    }
  },
  mounted () {
    this.$watch(this.key, 'methodName') // 型が合っているかわからない
  },
  methods: {
    methodName () {
    // something
    }
  }
}
```

TS 1.xではこのmounted内部の `this` を明示できず、`this` は  `any` 型になってしまいました。
TS 2.0からは、以下のように記述でき、`function` 内部の `this` の型を明示できるようになりました。例えばこんな感じです。

```ts
import * as Vue from 'vue'

export interface MyComponent extends Vue {
  key: string
}

export default {
  mounted (this: MyComponent) {
    this.$watch(this.key, 'methodName')
  }
}
```

型定義ファイルには、`interface ComponentOptions<V extends Vue>` が定義されています。これを利用することで、`ComponentOptions` 内部のすべての `this` を自動的に明示できるようになります。以下のような感じで使います。

```ts
import { ComponentOptions } from 'vue'

export interface MyComponent extends Vue {
  key: string
}

export default {
  mounted () {
    this.$watch(this.key, 'methodName')
  },
  beforeDestroy() {
    this.$off('key')
  }
} as ComponentOptions<MyComponent>
```

また、後述するように、JavaScriptだけではなくHTMLやCSSなどに対しても型定義ファイルを作れるようになったことも嬉しいです。

# 作るもの
昨年と同様、サンプルアプリケーションを作ろうと思います。
明日12月4日は、『ご注文はうさぎですか?』に登場するキャラクター「香風智乃」の誕生日であります。したがって彼女の誕生日をお祝いするアプリケーションを作ろうと思います。
昨年との変更点は、Vuexを導入したことと、jQueryを使わなくなったことと、データの永続化にDexie.jsを使ったことです。

# 方法

* TypeScriptで記述する
* テンプレートはHTMLファイルに記述する
* CSSはSCSSで記述する
* Webpackを使う

## デコレータについて
`vue-class-component` 以外にも色々あります。
2.0に対応しているものをリストアップします。

* https://github.com/HerringtonDarkholme/av-ts
* https://github.com/locoslab/vue-typescript-component
* https://github.com/wonderful-panda/vueit
* https://github.com/InDIOS/vue-ts-decorate


今回はVue.js公式の`vue-class-component`を使います。
また、[vue-property-decorator](https://github.com/kaorun343/vue-property-decorator)も合わせて利用します。

## TypeScriptのヘルパー関数について

デコレータを使うと、ヘルパー関数が何度も生成されてファイルサイズが増えてしまいます。
TypeScript2.1からは、[tslib](https://github.com/Microsoft/tslib) を使うことで防ぐことができます。
[TypeScript 2.1.1 変更点 - Qiita](http://qiita.com/vvakame/items/305749d3d6dc6bf877c6)によると、

1. `tslib` をnpmなどでダウンロードする。
2. `tsconfig.json` 内の `importHelpers` と `noEmitDecorators` を `true` にする。

のようにします。

## Viewについて
2.0より前のバージョンでは、`template` プロパティにHTMLを記述したりテンプレートのidを記述したりしていました。
一方2.0からは仮想DOMが採用されました。したがって仮想DOMを構築する関数の中身を用意することが必要となりました。これは日本語ドキュメントにおいて[描画関数](https://jp.vuejs.org/v2/guide/render-function.html)と訳されています。

Vue.jsは現在2.1が最新バージョンですが、https://github.com/vuejs/vue/tree/dev/dist をみるといっぱいビルドファイルが並んでいます。注目していただきたいのは、`compiler-included` と `runtime-only` の違いです。前者はテンプレートから描画関数を生成するコンパイラが含まれていますが、後者には含まれていません。
したがって、後者を利用するときはいかにして `render` 及び `staticRenderFn` といった描画関数を用意するかがポイントになります。

### 自分で描画関数を書く
デコレータ内部に記述するか、クラス定義の中に記述します。
型チェックが効くのが嬉しいです。

```ts
import * as Vue from 'vue'
import Component from 'vue-class-component'

@Component<Vue>({
  render (h) {
    return h('div', this.message)
  }
})
class MyComponent extends Vue {

  message = 'Hello world'

  render (h: Vue.CreateElement) {
    return h('div', this.message)
  }
}

```

### WebpackのloaderでHTMLをコンパイルする
HTMLファイルからVue.jsの描画関数を生成するWebpackのloaderは２つあります。

* https://www.npmjs.com/package/vue-template-loader
* https://www.npmjs.com/package/vue-template-compiler-loader

この２つの違いは、`render` と `staticRenderFn` をどのように提供しているかということです。
ここで、型定義ファイルを書いてみることにします。

```ts:vue-template-loader
declare module '*.html' {
  import * as Vue from 'vue'
  import { ComponentOptions } from 'vue'
  
  var template: <V extends Vue>(options: ComponentOptions<V>) => ComponentOptions<V>

  export = template
}


import * as Vue from 'vue'
import Component from 'vue-class-component'
import * as template from 'my-template.html'

// thisを明示するときは、 @Component(template<MyComponent>({}))と書きます。
@Component(template({}))
class MyComponent extends Vue {
}
```

```ts:vue-template-compiler-loader
declare module '*.html' {
  import * as Vue from 'vue'
  import { CreateElement, VNode } from 'vue'
  
  export var render: (this: Vue, createElement: CreateElement) => VNode
  export var staticRenderFns: ((createElement: CreateElement) => VNode)[]
}



import * as Vue from 'vue'
import Component from 'vue-class-component'
import { render, staticRenderFns } from 'my-template.html'

@Component({ render, staticRenderFns })
class MyComponent extends Vue {
}

// もしくは
import * as template from 'my-template.html'

@Component({
  mixins: [template]
})
class MyComponent extends Vue {
}
```


今回は前者を使いました。

HTMLWebpackPluginと併用すると、エラーを吐いてしまいました。したがってコンポーネントのテンプレートは拡張子を`*.template.html`としました。

### シングルファイルコンポーネントを使う
`ts-loader`と組み合わせることで、`*.vue`内でTypeScriptを使えるようです。

# 作ったもの
[ここ](https://github.com/kaorun343/vue-typescript-example-2)にあります。
ダウンロードして、`dist/index.html`をブラウザで開いてみてください。

# 考察
よくできたと思います。

# 最後に
## 最近のVue.jsについて
以前は数あるその他フロントエンドフレームワーク的な存在だったVue.jsは、今やGitHubのスター数で[こんな感じ](https://github.com/showcases/front-end-javascript-frameworks)になっています。0.10の頃から注目していたので、知名度が大きくなったことはとても嬉しく思っています。

## コードネームについて
2.0は攻殻機動隊 (Ghost in the Shell) でした。
次はHUNTER×HUNTERとかどうでしょう。
また1.1はFになるようですが、蒼穹のファフナーか鋼の錬金術師になると信じています。


---
明日12月4日は@nasumさんです。