はじめてのvue-property-decorator (nuxtにも対応）

この記事はVueを勉強している段階からTypeScriptでクラスベースのVueアプリを作りたい！という方へ向けて例を交えながら[`vue-property-decorator`](https://github.com/kaorun343/vue-property-decorator)の機能を**基本**と**応用**、**上級**の３セクションに分けて説明していきます。

基本では、Vueでアプリを作る上で必須となる機能を、応用では用意されている便利なデコレータを、上級では普通の開発ではほぼ使わない機能について説明します。
とりあえずは基本のみ理解しておけば困ることはないでしょう。

また、最後に[`nuxt-property-decorator`](https://github.com/nuxt-community/nuxt-property-decorator)独自のデコレータも紹介しています。

**2019/12/09 追記**：`nuxt-property-decorator`独自のデコレータの紹介を追加しました

## 動作確認バージョン

* [`vue-property-decorator v8.4.2`](https://github.com/kaorun343/vue-property-decorator/tree/v8.4.2)
* [`nuxt-property-decorator v2.5.1`](https://github.com/nuxt-community/nuxt-property-decorator/tree/v2.5.1)
  - `nuxt-property-decorator v2.7.2` で vuex-module-decorators が同梱されるようになりました（追記予定）

## 基本

### コンポーネントの定義

`@Component`は続けて定義しているクラスをVueが認識できる形式に変換しています。

以下の２つは同じ意味です。

```vue
<script>
export default {
  name: 'SampleComponent'
};
</script>
```

```vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {}
</script>
```

この時、`vue-property-decorator`の`Vue`クラスを継承することを忘れないように気をつけてください。

### Data

Dataはクラスのメンバーとして定義するだけで利用できます。

以下のサンプルでは名前と年齢をDataに持たせています。

```vue

<script>
export default {
  data() {
    return {
      name: 'simochee',
      age: 21
    }
  }
};
</script>
```

```vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  name = 'simochee';
  age = 21;
}
</script>
```

Dataをテンプレート内で使用するときはプレーンなVueと同じように参照できます。

```vue
<template>
  <!-- simochee (21) -->
  <p>{{ name }} ({{ age }})</p>
</template>
```

### Computed

算出プロパティ（Computed）はクラスのGetterとして定義することで利用できます。

以下のサンプルではDataに定義されたスコアを３倍する算出プロパティを定義しています。

```vue
<script>
export default {
  data() {
    return {
      score: 55
    }
  },
  computed: {
    triple() {
      return this.score * 3;
    }
  }
};
</script>
```

```vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  score = 55;

  get triple() {
    return this.score * 3;
  }
}
</script>
```

Computedをテンプレート内で使用するときはプレーンなVueと同じように参照できます。

```vue
<template>
  <!-- Triple score: 163! -->
  <p>Triple score: {{ triple }}!</p>
</template>
```

### メソッド

メソッドはクラスのメソッドとして定義するだけで利用することができます。

以下の例では、ボタンが押されたときに`onClickButton`メソッドを呼び出しています。

```vue
<template>
  <button @click="onClickButton">Click Me!</button>
</template>
```

このようなテンプレートがあったときの`onClickButton`は以下のように定義できます。

```vue
<script>
export deafult {
  methods: {
    onClickButton() {
      // ボタンが押されたときの処理
    }
  }
};
</script>
```

```vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  onClickButton() {
    // ボタンが押されたときの処理
  }
}
</script>
```

Reactのようにメソッドに`this`をバインドする必要はありません。

### ライフサイクルフック

[ライフサイクルフック](https://jp.vuejs.org/v2/api/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF)は、クラスにライフサイクルの名前でメソッドを定義するだけで利用できます。

```vue
<script>
export default {
  mounted() {
    // コンポーネントがマウントされたときの処理
  },
  beforeDestroy() {
    // コンポーネントが破棄される直前の処理
  }
}
</script>
```

```vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  mounted() {
    // コンポーネントがマウントされたときの処理
  }

  beforeDestroy() {
    // コンポーネントが破棄される直前の処理
  }
}
</script>
```

`vue-property-decorator`ではライフサイクルフックとメソッドが同じ領域で定義されるため、ライフサイクルの名前でメソッドを定義しないように注意が必要です。

### `@Component`

`@Component`は引数としてVueのオブジェクトを指定することができます。

以降で各種デコレータを紹介しますが、そこで定義できない`components`や`filters`、`mixins`などのプロパティは`@Component`の引数として指定します。

```vue
<script>
export deafult {
  components: {
    AppButton,
    ProductList
  },
  directives: {
    resize
  },
  filters: {
    dateFormat
  },
  mixins: [
    PageMixin
  ]
};
</script>
```

```vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component({
  components: {
    AppButton,
    ProductList
  },
  directives: {
    resize
  },
  filters: {
    dateFormat
  },
  mixins: [
    PageMixin
  ]
})
export default class SampleComponent extends Vue {
}
</script>
```

他にも、以下のドキュメントで紹介されているプロパティを指定できます。

* [オプション / データ](https://jp.vuejs.org/v2/api/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-%E3%83%87%E3%83%BC%E3%82%BF)
* [オプション / DOM](https://jp.vuejs.org/v2/api/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-DOM)
* [オプション / ライフサイクルフック](https://jp.vuejs.org/v2/api/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF)
* [オプション / アセット](https://jp.vuejs.org/v2/api/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-%E3%82%A2%E3%82%BB%E3%83%83%E3%83%88)
* [オプション / 構成](https://jp.vuejs.org/v2/api/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-%E6%A7%8B%E6%88%90)
* [オプション / その他](https://jp.vuejs.org/v2/api/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-%E3%81%9D%E3%81%AE%E4%BB%96)

### `@Prop`

`@Prop`は続けて定義したメンバーを`props`として使用できるようにします。

親コンポーネントからは定義したメンバー名で`props`を指定できます。

```vue
<script>
export deafult {
  props: {
    userName: {
      type: String,
      required: true
    },
    isVisible: {
      type: Boolean,
      default: false
    }
  }
};
</script>
```

```vue
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  @Prop({ type: String, required: true })
  userName: string;

  @Prop({ type: Boolean, defualt: false })
  isVisible: boolean;
}
</script>
```

`@Prop`の引数には`props`で指定可能なオプションがすべて指定できます。

* [props](https://jp.vuejs.org/v2/api/#props)

### `@Watch`

`@Watch`は第１引数に監視したい値へのパスを、第２引数にウォッチャのオプションを指定できます。

以下のサンプルでは単一のDataとObjectのプロパティの値を監視するメソッドを定義しています。

なお、`immediate: true`はコンポーネント初期化時にも処理を実行するかを指定するオプションです。

```vue
<script>
export deafult {
  data() {
    isLoading: false,
    profile: {
      name: 'simochee',
      age: 21
    }
  },
  watch: {
    isLoading() {
      // ローディング状態が切り替わったときの処理
    },
    'profile.age': {
      handler: function() {
        // プロフィールの年齢が変更されたときの処理
      },
      immediate: true
    } 
  }
};
</script>
```

```vue
<script lang="ts">
import { Component, Watch, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  isLoading = false;
  profile = {
    name: 'simochee',
    age: 21
  };

  @Watch('isLoading')
  onChangeLoadingStatus() {
    // ローディング状態が切り替わったときの処理
  }

  @Watch('profile.age', { immediate: true })
  onChangeProfileAge() {
    // プロフィールの年齢が変更されたときの処理
  }
}
</script>
```

Vueの仕様からも分かる通り、`@Watch`は同じパスに対して複数回指定することはできません。
複数回指定された場合は後勝ちとなるため、先に定義したウォッチャは実行されません。

## 応用

### `@PropSync`

Vue.jsでは`props`を指定する際に`.sync`修飾子を付与することで、子コンポーネントから親コンポーネントの値を変更することができるようになります。

仕組みとしては、`@update:<Prop名>`というイベントを受け取ったらDataに代入するという処理を暗黙的に行っています。

```vue
// 親コンポーネント
<template>
  <!-- 以下の２つは同じ意味 -->
  <ChildComponent
　  :childValue.sync="value"
  />
  <ChildComponent
    :childValue="value"
    @update:childValue="value = $event"
  />
</template>
```

このとき、子コンポーネント以降で`.sync`プロパティをバケツリレーする際に便利なのが`@PropSync`デコレータです。

このデコレータを使用しない場合は、以下のように書かなければいけませんでした。

```vue
// 子コンポーネント
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  @Prop({ type: String })
  childValue: string;

  // value を変更したいときに呼び出す
  updateValue(newValue) {
    this.$emit('update:childValue', newValue);
  }
}
</script>
```

これが、`@PropSync`で定義した場合はメンバーへ値を代入するだけで同等の処理を実現することができます。

```vue
<script lang="ts">
import { Component, PropSync, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  @PropSync({ type: String })
  childValue: string;

  // value を変更したいときに呼び出す
  updateValue(newValue) {
    this.childValue = newValue
  }
}
</script>
```

代入するだけで値の変更を通知できるため、さらに`.sync`で孫コンポーネントへ値を渡す場合なども、とてもシンプルに書くことができます。

```vue
<template>
  <SunComponent
    :sunValue.sync="childValue"
  />
</template>
```

### `@Emit`

Vueではコンポーネント間での値を双方向にやり取りすることができます。

親から子への値渡しはPropを指定することで行い、子から親への値渡しはイベントを通知することによってアクションや値を受け渡せるようになっています。

このとき、子から親へ値を渡すときのイベントを発行するのが`$emit`メソッドです。

以下のサンプルでは子コンポーネントで送信処理が行われたこと`submit`イベントとして親コンポーネントへ通知し、受け取った値を元に親コンポーネント側でリクエストを送信しています。

```vue
// 子コンポーネント
<template>
  <form @submit="onSubmit">
    <input v-model="value">
    <button type="submit">Submit</button>
  </submit>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class ChildComponent extends Vue {
  value = '';

  // 値を送信する処理
  onSubmit() {
    this.$emit('submit', this.value);
  }
}
</script>
```

```vue
// 親コンポーネント
<template>
  <ChildComponent
    @submit="onReceiveSubmit"
  />
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ChildComponent from './ChildComponent.vue';

@Component({
  components: {
    ChildComponent
  }
})
export default class ParentComponent extends Vue {
  async onReceiveSubmit(newValue: string) {
    // $emitでの第２引数を受け取ることができる
    await this.$request.post(newValue);
  }
}
</script>
```

`@Emit`では`$emit`の処理を事前に定義することができます。

イベント名は`@Emit`の第１引数に明示的に指定するか、省略した場合は続けて定義しているメソッド名が利用されます。

また、メソッドで値を返却すれば`$emit`でその値を送ることもできます。

上記のサンプルの子コンポーネントを`@Emit`で書き換えると以下のようになります。

```vue
<template>
  <form @submit="submit">
    <input v-model="value">
    <button type="submit">Submit</button>
  </submit>
</template>

<script lang="ts">
import { Component, Emit, Vue } from 'vue-property-decorator';

@Component
export default class ChildComponent extends Vue {
  value = '';

  // 値を送信する処理
  // イベント名を指定しない場合でも () は省略できない
  @Emit()
  submit() {
    return this.value;
  }
}
</script>
```

この他に、`@Emit`を非同期メソッドへ設定することもできます。

なお、キャメルケースでイベント名、メソッド名を指定した場合、親コンポーネントで受け取る際にはケバブケースへ変換されますので注意が必要です。

```ts
// 子コンポーネント
@Emit()
submitForm() {}
```

```vue
// 親コンポーネント
<ChildComponent
  @submit-form="onSubmit"
  @submitForm="onSubmit" // 発火しない
/>
```

親コンポーネントでケバブケースのイベントを受け取りたい場合は、デコレータの引数にイベント名を指定する必要があります。

```ts
// 子コンポーネント
@Emit('submitForm')
submitForm() {}
```

### `@Ref`

`@Ref`は`$refs`で参照できる要素、コンポーネントの型を定義します。
事前に定義しておくことでタイポや変更へ対応しやすくなります。

```vue
<template>
  <ChildComponent ref="childComponent" />
  <button ref="submitButton">Submit</button>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ChildComponent from '@/component/ChildComponent.vue';

@Component({
  components: {
    ChildComponent
  }
});
export default class SampleComponent extends Vue {
  @Ref() childComponent: ChildComponent;
  @Ref() submitButton: HTMLButtonElement;

  mounted() {
    // 子コンポーネントのメソッドを実行する
    this.childComponent.updateValue();

    // ボタンをフォーカスする
    this.submitButton.focus();
  }
}
</script>
```

## 上級

以降は上級者向けのため、あまり詳しく記載しません。必要であれば公式ドキュメントなどを参照してください。

### `@Model`

VueのModelを定義します。VueではModelを指定する際にPropも定義しそちらに型情報などを記載しなければいけませんでしたが、デコレータでは`@Model`に併せて定義できます。

> [Vue.js公式ドキュメント#model](https://jp.vuejs.org/v2/api/#model)

以下の２つは同じ意味です。

```vue
<script>
export deafult {
  props: {
    value: {
      type: String,
      required: true
    }
  },
  model: {
    prop: 'value',
    event: 'update'
  }
};
</script>
```

```vue
<script lang="ts">
import { Component, Model, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  @Model('update', { type: String, required: true })
  value: string;
}
</script>
```

このとき、暗黙的に`value`がPropとして定義されるため`value`という`data`や`methods`は定義できません。

### `@Provide` / `@Inject`

Vueでは親で`provide`として定義した値をその子要素（親子でなくても良い）から`inject`で参照することができます。

> [Vue.js公式ドキュメント#inject/provide](https://jp.vuejs.org/v2/api/#provide-inject)

以下の２つは同じ意味です。

```vue
<!-- Parent.vue -->
<script>
export deafult {
  provide: {
    foo: 'foo',
    bar: 'bar'
  }
};
</script>

<!-- Child.vue -->
<script>
export deafult {
  inject: {
    foo: 'foo',
    bar: 'bar',
    optional: { from: 'optional', default: 'default' }
  }
};
</script>
```

```vue
<!-- Parent.vue -->
<script lang="ts">
import { Component, Provide, Vue } from 'vue-property-decorator';

@Component
export default class ParentComponent extends Vue {
  @Provide() foo = 'foo';
  @Provide('bar') baz = 'bar';
}
</script>

<!-- Child.vue -->
<script lang="ts">
import { Component, Inject, Vue } from 'vue-property-decorator';

@Component
export default class ChildComponent extends Vue {
  @Inject() foo: string;
  @Inject('bar') bar: string;
  @Inject({ from: 'optional', default: 'default' }) optional: string;
  @Inject(symbol) baz: string;
}
</script>
```

### `@ProvideReactive` / `@ProvideInject`

`@Provide` / `@Inject`の拡張です。親コンポーネントから`@ProvideReactive`として提供された値の変更を子コンポーネントでキャッチできるようになります。

```vue
<!-- Parent.vue -->
<script lang="ts">
import { Component, ProvideReactive, Vue } from 'vue-property-decorator';

@Component
export default class ParentComponent extends Vue {
  @ProvideReactive() foo = 'foo';
}
</script>

<!-- Child.vue -->
<script lang="ts">
import { Component, InjectReactive, Vue } from 'vue-property-decorator';

@Component
export default class ChildComponent extends Vue {
  @InjectReactive() foo: string;
}
</script>
```

### `readonly`と`!`、`?`について

`vue-property-decorator`のサンプルコードでは`readonly`や`prop!: String`のような`!`が登場しています。

これらはいずれもTypeScriptの機能です。

`readonly`修飾子は、メンバー変数を読み込み専用とするためのものです。
VueではProp、Modelへの直接の代入はエラーとなります。

誤った代入を事前に防ぐために`@Prop`および`@Model`で定義したメンバー変数へは`readonly`修飾子を付与することをおすすめします。

```vue
<script lang="ts">
import { Component, Prop, PropSync, Watch, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  @Prop({ type: String }) readonly name: string;
  @Model('update', { type: Object }) readonly profile: IProfile;
  @PropSync({ type: String }) value: string; // 代入可能
}
</script>
```

また、デコレータで定義されたすべてのメンバー変数についている`!`は [NonNullAssertionオペレータ](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator) と呼ばれる機能です。
`!`がついたプロパティがNull/Undefinedではないことを明示します。

ただし、`!`は`required: true`またはデフォルト値が設定されているプロパティにのみ指定することをおすすめします。
逆に、必須項目でなくデフォルト値も指定されていない場合は、`?`を指定することをおすすめします。

この`?`はプロパティが任意項目であり、`undefined`の可能性があることを明示します。

```vue
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class SampleComponent extends Vue {
  @Prop({ type: String, required: true })
  readonly name!: string;

  @Prop({ type: Array, default: () => [] })
  readonly items!: string[];

  @Prop({ type: Object });
  readonly profile?: IProfile;

  mounted() {
    // undefinedの可能性のあるオブジェクトのプロパティを
    // 参照しようとしたのでタイプエラーになる
    profile.age;
  }
}
</script>
```

より強固に型安全にしたい場合はこれらに気をつけて開発するようにしてみてください。

## `nuxt-property-decorator`

`nuxt-property-decorator`は`nuxt-community`がメンテナンスを行っている`vue-property-decorator`のラッパーです。
`vue-property-decorator`の機能に加え、Nuxt独自のライフサイクルメソッドやプロパティの対応などが追加されています。

また、`nuxt-property-decorator`には`vue-property-decorator`にない独自のデコレータが実装されています。

### `@On` / `@Off` / `@Once`

Vueでは`this.$emit`したイベントを`this.$on`で検知することができます。

以下のソースは、`mounted`時に`foo`イベントを待機させる`$on`/`$once`を仕込み、`onClick`メソッド内で`foo`イベントを発火させています。
`onClick`メソッドが実行されると、必ず`callback`メソッドも実行され、最初の１度のみ`onceCallback`メソッドも実行されます。

また、`clearCallback`メソッドが呼ばれると`foo`イベントに対し設定されたコールバックが`$off`によって解除され、以降は発火しなくなります。

```ts
<script>
export default {
  name: 'SampleComponent',
  created() {
    // fooイベントを待機
    this.$on('foo', this.callback);
    // fooイベントを１度だけ待機
    this.$once('foo', this.onceCallback);
  },
  methods: {
    callback(name) { ... },
    onceCallback(name) { ... },
    onClick(user) {
      this.$emit('foo', user.name);
    },
    clearCallback() {
      this.$off('foo', this.callback);
    }
  },
};
</script>
```

これを`nuxt-property-decorator`の`@On`/`@Off`/`@Once`デコレータで書き換えると以下のようになります。

```ts
<script lang="ts">
import { Component, Emit, On, Off, Once, Vue } from 'nuxt-property-decorator';

@Component
export default class SampleComponent extends Vue {
  @On('foo')
  callback(name: string): void { ... }

  @Once('foo')
  onceCallback(name: string): void { ... }

  @Off('foo', 'callback')
  clearCallback(): void { }

  @Emit('foo')
  onClick(user: IUser): string {
    return user.name;
  }
}
</script>
```

VueのAPIと異なるのは、`@Off`で指定するのがコールバック関数そのものではなく、その名前であるという点です。
上記の例では`this.callback`メソッドのイベントリスナを解除したいので`'callback'`を指定しています。

### `@NextTick`

Vueの`this.$nextTick`メソッドは、次の処理を描画が終わってから実行するときに使用します。

```vue
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'nuxt-property-decorator';

@Component
export default class SampleComponent extends Vue {
  /** コンテンツ */
  content = '';

  /** コンテンツの高さ */
  contentHeight = 0;

  /** コンテンツの要素のRef */
  @Ref('content') contentElement: HTMLParagraphElement;

  /**
   * コンテンツの変更と高さの変更
   * @param newContent 新しいコンテンツ
   */
  async onChangeContent(newContent: string): Promise<void> {
    this.content = newContent;

    // 新しいコンテンツ反映後の高さを取るため描画完了を待機
    await this.$nextTick();

    this.contentHeight = this.$ref.contentElement.clientHeight;
  }
}
</script>
```

これが、`@NextTick`デコレータを使うと以下のようになります。

`@NextTick`デコレータは付与したメソッドの実行が完了したあとに、引数で指定したメソッドを呼ぶようにして使用します。

```ts
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'nuxt-property-decorator';

@Component
export default class SampleComponent extends Vue {
  /** コンテンツ */
  content = '';

  /** コンテンツの高さ */
  contentHeight = 0;

  /** コンテンツの要素のRef */
  @Ref('content') contentElement: HTMLParagraphElement;

  /**
   * コンテンツの変更と高さの変更
   * @param newContent 新しいコンテンツ
   */
  @NextTick('updateContentHeight')
  onChangeContent(newContent: string): void {
    this.content = newContent;
  }

  /**
   * コンテンツの高さを更新する
   */
  updateContentHeight(): void {
    this.contentHeight = this.$ref.contentElement.clientHeight;
  }
}
</script>
```