Vue.js + TypeScriptではフィールドの初期化に注意

本記事は、Vue.js #2 Advent Calendar 2018 13日目の記事です。

現在、業務でVue.js + TypeScriptを使ってフロントを構築しています。
そのなかで地味にはまった点を紹介します。

一言で言うと、「TypeScriptでクラスを宣言するときにフィールドを初期化しないとVue.jsでリアクティブにならないよ」ということです。

# 問題のコード

はまったときのコードは以下のような感じのコードです。

```html
<template lang="pug">
v-app#app
  v-toolbar(app color="primary" dark)
    v-toolbar-title Vue.js #2 Advent Calendar 2018
  v-content
    v-container(grid-list-md)
      v-layout(row wrap)
        v-flex(xs12)
          v-card
            v-card-title カウンター
            v-card-text {{ state }}
            v-card-actions
              v-spacer
              v-btn(@click="up()" flat color="primary") count up
</template>

<script lang="ts">
class CounterState {
  name = "CounterState";
  count: number;
}

const state = new CounterState();

new Vue({
  el: "#app",
  data: () => ({
    state,
  }),
  methods: {
    up() {
      this.state.count++;
    }
  }
})
</script>
```

動かしてみるとわかりますが、ボタンを押しても画面が更新されません。(リアクティブになっていない）
何故？ :thinking_face:


# 原因

調べてみると、TypeScriptをJavaScriptにトランスパイルした結果がイメージと違っていて、Vue.jsが値の変更を検知できていないことが問題でした。


## Vue.jsの制約

Vue.jsは初期化時にフィールドが存在していないとリアクティブにならないという制約があります。

[リアクティブプロパティの宣言 - Vue.js](https://jp.vuejs.org/v2/guide/reactivity.html#%E3%83%AA%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E5%AE%A3%E8%A8%80)

> Vue では新しいルートレベルのリアクティブなプロパティを動的に追加することはできないため、インスタンスの初期化時に前もって全てのルートレベルのリアクティブな data プロパティを宣言する必要があります。空の値でもかまいません

これはVue.jsを使っていれば最初のほうにぶつかる制約かと思います。
(オブジェクトに新しいフィールドを増やす時は、`Vue.set(target, key, value)`を使わないといけないやつ。)


## TypeScriptからJavaScriptへのトランスパイル

今回の例では、トランスパイルした結果、`count`フィールドが存在しないことになってしまっているためうまく動きませんでした。
実際に`CounterState`の部分をトランスパイルした結果はこうなります。

```js
var CounterState = /** @class */ (function () {
    function CounterState() {
        this.name = "CounterState";
    }
    return CounterState;
}());
var state = new CounterState();
```

`count`が綺麗さっぱりなくなっていますね。
(フィールドさえ定義していれば、`null`が入るかと思っていたJava脳です。)



# 他のパターン

TypeScriptでの初期化の例をいくつか、あげてみました。
JavaScriptに変換されるとどうなるかイメージできますでしょうか。

```ts
interface Counter {
  count: number;
}

class CounterNone implements Counter {
  count: number;
}

class CounterNull implements Counter {
  count: number = null;
}

class CounterUndefined implements Counter {
  count: number = undefined;
}

class CounterDefault implements Counter {
  count: number = 0;
}

class CounterConstructor implements Counter {
  count: number;
  constructor() {
    this.count = 0;
  }
}
```

各クラスの違いとしては`count`フィールドに対して設定しているものが違います。



結果はこちら。

```js
var CounterNone = /** @class */ (function () {
    function CounterNone() {
    }
    return CounterNone;
}());
var CounterNull = /** @class */ (function () {
    function CounterNull() {
        this.count = null;
    }
    return CounterNull;
}());
var CounterUndefined = /** @class */ (function () {
    function CounterUndefined() {
        this.count = undefined;
    }
    return CounterUndefined;
}());
var CounterDefault = /** @class */ (function () {
    function CounterDefault() {
        this.count = 0;
    }
    return CounterDefault;
}());
var CounterConstructor = /** @class */ (function () {
    function CounterConstructor() {
        this.count = 0;
    }
    return CounterConstructor;
}());
```

Vue.jsにのせたときのサンプルを作成してみました。

<p data-height="500" data-theme-id="0" data-slug-hash="Jwddmw" data-default-tab="js,result" data-user="totto357" data-pen-title="Vue TypeScript Reactivity" class="codepen">See the Pen <a href="https://codepen.io/totto357/pen/Jwddmw/">Vue TypeScript Reactivity</a> by totto357 (<a href="https://codepen.io/totto357">@totto357</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


いかがでしょうか。
Vue.jsで扱う際はしっかりと初期化するようにしましょう。(戒め)