Vue.js+TypeScriptのススメ

# はじめに
いつもと違う環境でアプリを作りたくなり、Vue.js+TypeScriptでアプリを作ってみました。
[Vue](https://jp.vuejs.org/)のみ or [TypeScript](https://www.typescriptlang.org/)のみの情報は結構見つかりましたが、Vue+TypeScriptについてがっつり書いてるものが無かったので自分で記事を書いてみることにしました。
この記事では分かりやすいようにコードを比較する形で書いてます。
※JavaScriptのコード例はTypeScriptから逆算して書いたので間違ってるかもしれません。その際は指摘してもらえると助かります。

### 勧める理由
VueとTypeScript(正確には`vue-class-component`や`vue-property-decorator`も含む)を併用することで、型システムやデコレーター等を用いて型安全なコードを簡潔に書けるためです。TypeScriptはいいぞ。

# 作ったもの
今回はCoD(FPSの洋ゲー)の戦績閲覧アプリをElectronで作りました。
アカウントを入力すると戦績が表示されます。モード毎の戦績とマップごとの戦績があり、モードやマップを絞り込んだり戦績順にソートしたりできます。また下にはサマリ行があり、カーソルを重ねると円グラフがポップアップします。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Qiita用 <a href="https://t.co/WWiodgQ3ML">pic.twitter.com/WWiodgQ3ML</a></p>&mdash; なつ@VR (@VRNatsuVR) <a href="https://twitter.com/VRNatsuVR/status/1102102015537111040?ref_src=twsrc%5Etfw">2019年3月3日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# TypeScriptでのVueクラスの書き方
まずは(トランスパイラを使わない)JavaScriptとTypeScriptを比較してみましょう。以下は単一ファイルコンポーネントのスクリプト部分です。

まずはJavaScript。

```JavaScript
module.exports = {
  // 型と初期値を持つprops
  props: {
    propMessage: {
      type: String,
      default: ""
    }
  }

  data: function() {
    return {
      message: 123 // 初期データ
    }
  }

  // 算出プロパティ
  computed: {
    computedMessage: function() {
      return 'computed ' + this.message;
    }
  }

  // 監視プロパティ
  watch: {
    message: {
      handler: function(newMessage, oldMessage) { /*処理*/ },
      immediate: true,
      deep: true
    }
  }

  // ライフサイクルフック
  mounted: function() {
    this.greet();
  }

  // 普通のメソッド
  methods: {
    greet: function() {
      alert('greeting: ' + this.message);
    }
  }
}
```

次にTypeScript。

```TypeScript
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
  // 型と初期値を持つpropsと監視プロパティ
  props: {
    propMessage: {
      type: String,
      default: ""
    }
  },
  watch: {
    "message": [
      {
        handler: "onMessageChanged",
        immediate: true,
        deep: true
      }
    ]
  },
  methods: {
    onMessageChanged(newMessage: string, oldMessage: string): string { /*処理*/ }
  }
})
export default class App extends Vue {
  // 初期データをクラスのプロパティとして記述できる
  message: string = 123;

  // 算出プロパティは読み取り専用プロパティとして記述できる
  get computedMessage(): string => 'computed ' + this.message;

  // ライフサイクルフックをクラスメソッドとして記述できる
  mounted(): void => this.greet();

  // 普通のメソッドもクラスメソッドとして記述できる
  greet(): void => alert('greeting: ' + this.message);
}
```

上の例で分かるかもしれませんが、JavaScriptでは以下のような制約がありコードを簡潔に書けません。

- `data`をメソッドの戻り値として定義する必要がある
- `watch`、`methods`、ライフサイクルフック(`mounted`等)でアロー演算子が非推奨

TypeScriptでは`data`はクラスのプロパティで書けますし、アロー演算子も使えるため簡潔にコードを書けます。更にプロパティ・メソッドの引数や戻り値に型を指定することができ、型安全にコードを書けます。

# Vue Property Decoratorを用いた記述
TypeScriptはJavaScriptと比較して簡潔にコードが書けることがわかったかと思いますが、**props**と**watch**が少し冗長と思ったかも知れません。それを解決するのが[vue-property-decorator](https://github.com/kaorun343/vue-property-decorator)です。
`vue-property-decorator`を導入することで、`props`と`watch`は以下のように書き換えることが出来ます。

```TypeScript
@Component({
  props: {
    propMessage: {
      type: String,
      default: ""
    }
  },
  watch: {
    "message": [
      {
        handler: "onMessageChanged",
        immediate: true,
        deep: true
      }
    ]
  },
  methods: {
    onMessageChanged(newMessage, oldMessage): string { /*処理*/ }
  }
})
```

```TypeScript
@Prop({ default: "" }) propMessage!: string
@Watch("Message", { immediate: true, deep: true })
onMessageChanged(newMessage: string, oldMessage: string): string { /*処理*/ }
```

これで**props**と**watch**がかなり簡潔に書けました。20行のコードが3行になりました。

# まとめ
最後に、改めてJavaScriptとTypeScriptのコード全体を比較してみましょう。
まずJavaScript。

```JavaScript
module.exports = {
  // 型と初期値を持つprops
  props: {
    propMessage: {
      type: String,
      default: ""
    }
  }

  data: function() {
    return {
      message: 123 // 初期データ
    }
  }

  // 算出プロパティ
  computed: {
    computedMessage: function() {
      return 'computed ' + this.message;
    }
  }

  // 監視プロパティ
  watch: {
    message: {
      handler: function(newMessage, oldMessage) { /*処理*/ },
      immediate: true,
      deep: true
    }
  }

  // ライフサイクルフック
  mounted: function() {
    this.greet();
  }

  // 普通のメソッド
  methods: {
    greet: function() {
      alert('greeting: ' + this.message);
    }
  }
}
```

次にTypeScript。

```TypeScript
import Vue from 'vue'
import { Component, Vue, Prop, Watch } from "vue-property-decorator";

@Component
export default class App extends Vue {
  @Prop({ default: "" }) propMessage!: string
  @Watch("Message", { immediate: true, deep: true })
  onMessageChanged(newMessage: string, oldMessage: string): string { /*処理*/ }

  // 初期データをクラスのプロパティとして記述できる
  message: string = 123;

  // 算出プロパティは読み取り専用プロパティとして記述できる
  get computedMessage(): string => 'computed ' + this.message;

  // ライフサイクルフックをクラスメソッドとして記述できる
  mounted(): void => this.greet();

  // 普通のメソッドもクラスメソッドとして記述できる
  greet(): void => alert('greeting: ' + this.message);
}
```

比較しても分かる通りTypeScriptではコードを簡潔に、しかも型安全に書けます。
Vue+TypeScriptのススメ、**お分かりいただけただろうか？**


# おまけ
### フォームの内容を型安全にやりとりする方法
フォームを実装する際、簡単な[Storeパターン](https://jp.vuejs.org/v2/guide/state-management.html#%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AA%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%E3%82%92%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8B)を用いて内容を外部とやりとりしたいと思いました。その際、フォームの内容を型安全にやり取りしたいと思って試行錯誤した結果、以下のやり方に落ち着いたので紹介しておきます。
やり方は**フォームの内容を表すクラスを定義し、フォームの初期値をプロパティの初期値として定義する**ことです。こうすることでそのクラスのインスタンスを渡すことでフォームに初期値が設定できます。

```TypeScript
  import { Component, Vue, Prop } from "vue-property-decorator";

  export class InqueryFormFields {
    platform: string = "psn";
    id: string = "";
  }

  @Component
  export default class InqueryForm extends Vue {
    @Prop(InqueryFormFields) formInitialValues!: InqueryFormFields

    // formInitialValuesが存在する場合はformへ値渡しする
    form = this.formInitialValues ? Object.assign(new InqueryFormFields(), this.formInitialValues) : new InqueryFormFields();

    onSubmit(): void => this.$emit("submit", Object.assign(new InqueryFormFields(), this.form));
  }
```

(殆どのフォームがそうかと思いますが)上記の例では`InqueryFormFieldsインスタンス`の**参照**を渡すと意図しない挙動になるので、`Object.assign()`で`InqueryFormFieldsインスタンス`の**値**をコピーしています。
外部から渡されたインスタンスの値をコピーするのは`InqueryForm`の責務なので、`InqueryForm`内に実装を隠蔽する形にしています。こうすることで**外部のクラスがわざわざ値をコピーする必要がなくなります**。