Vue.js + Vuex + TypeScriptでStoreの値に型をつけるまでにやったこと

Vuex + TypeScriptの導入は、いろいろな方法が紹介されていますが、実際に動かしてみるとうまく動かないことも多く、ハードルが高い印象があります。

APIから受け取った値をStoreに格納する際、受け取る値に対して静的な型を導入したいと考えたときに、私がいちばん腑に落ちたのは、下記の方法でした。

- vue-class-componentで、クラスベースの記法にする
- vuex-module-decoratorsを導入し、Moduleを1つの大きなクラスとして扱う
- Moduleの中でinterfaceを使い、Storeの値に型をつける

ここからは、実際にTypeScriptをVuexに導入までの流れと、実装するときの注意点についてまとめています。

# サンプルについて
コード全体はこちらからご覧ください。
https://github.com/public-shibe23/sandbox-vue-ts-async

ローカルで確認をする場合は、下記コマンドを実行してください。

```
npm install
npm run demo
```

# 動作確認環境
vue-cli：3.5.2
node.js : 8.11.4
npm: 5.6.0


## vue-class-componentを使う
Vue.js + TypeScriptには、下記の2パターンがあります。

### Vue.extendの場合

```js:Home.vue
export default Vue.extend({
  name: "home",
  components: {
    ProductList
  }
 ... 
})
```
.vueファイルの、`export default { ... }`の部分を、`Vue.extend`に書き換える方法です。

気軽に導入できるメリットがありますが、Vuexを取り入れようとすると、エラーの解消がしづらく、特に$store周りの調整で、意図通りの挙動にならず、詰まることが多い印象でした。

### vue-class-componentの場合

```typescript:Home.vue
@Component({
  components: {
    ProductList
  }
})
export default class Home extends Vue {
  get products(){
    return ProductListModule.products
  }

  fetchProducts(): void {
    ProductListModule.FETCH_PRODUCTS();
  }
}
```
デコレータを使って、Angularのようなclassを使った書きかたができるようになります。
vue-cli3を使用している場合は、`vue create`したときに、`Use class-style component syntax?` と聞かれるので、Yesを選べばOKです。

## vuex-module-decoratorsを使う

vuex-module-decoratorsは、VuexのActions, Mutationなどを、先述のデコレータとして扱えるようにしたものです。

インストールは

```
npm install -D vuex-module-decorators
```

注意点として、ActionsやMutations、Getterなど、すべての要素が同じクラス内のプロパティ、またはメソッドとして定義されるため、数が増えたときに目的の値が重複したり、分かりづらくなる可能性があります。

# 実際にStoreの値に型をつけてみる
## 今回使用するデータ
```js
{
products: [{
    id: 1000;
    name: "T-shirts";
    stock: 100;
    price: 1000;
  },
   {
    id: 1000;
    name: "T-shirts";
    ....
  }]
}
```
簡単な商品一覧を想定して、`products`というプロパティの中に、各商品ごとの情報を配列で格納しています。
これらのidやpriceに対して、静的な型を導入します。

## interfaceの定義

```typescript
export interface IProductListState {
  products: IProductState[];
}
export interface IProductState {
  id: number;
  name: string;
  stock: number;
  price: number;
}
```
productsは、オブジェクトを配列形式で持っています。
ポイントは下記となります。
1. 配列内の1つ1つのオブジェクトが持っているkeyに型を定義する
2. 配列の親要素となるオブジェクトに、1のオブジェクトを配列として格納する


# Storeに型をつける
## Vuex側

```typescript
@Module({ dynamic: true, store, name: "productList", namespaced: true })
class ProductList extends VuexModule implements IProductListState {
  products: IProductState[] = [];

  @Action({ commit: "SET_ITEMS" })
  public async FETCH_PRODUCTS() {
    const products = await fetchProducts();
    return { products };
  }

  @Mutation
  public SET_ITEMS(payload: IProductListState) {
    this.products = payload.products;
  }
```
`implements IProductListState`とすることで、ProductList クラスは、必ずproductsというプロパティを保持することを強制することができます。

`@Action`は、引数として`{commit: [mutation名] }`という形式で
returnの値を指定した値でcommitすることができます。

async/awaitを使っていることによる注意事項はありませんが、戻り値をMutationのpayloadとして使える形式にする必要があるため、`{products}`にしています。


## コンポーネント側

```vue:src\views\Home.vue
<template>
  <div class="section">
    <div class="columns is-centered">
      <div class="column is-6">
        <ProductList :products="products" @fetch="fetchProducts"/>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import ProductList from "@/components/ProductList.vue";
import {ProductListModule} from '@/store/ProductList'

@Component({
  components: {
    ProductList
  }
})
export default class Home extends Vue {
  get products(){
    return ProductListModule.products
  }

  fetchProducts(): void {
    ProductListModule.FETCH_PRODUCTS();
  }
}
</script>
```

```vue:src\components\ProductList.vue
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IProductState } from "@/store/ProductList";

@Component
export default class ProductList extends Vue {
  @Prop() private products!: IProductState[]; // … [1]

  get totalPrice() {
    let total: number = 0;
    this.products.forEach((value, index) => {
      total += value.price;
    });
    return total;
  }

  fetch(): void {
    this.$emit("fetch");
  }
}
</script>
```
このコンポーネントは、Propsを親コンポーネントからproductsというStateを受け取っています。

productsには、先ほど作成したIProductStateを型として当てはめています。[1]
productsの中身は複数になる場合があるため、型はIProductState[]のように、配列にしておきます。

`totalPrice()`はAPIのレスポンスとして受け取ったproductsの値から、価格の合計値を求めるcomputedプロパティです。

型を指定したことにより、this.productsに入る値が配列であることがわかっているため、forEach()がエディタの補完機能でサジェストされる他、格納されているオブジェクトの中身も確認することができます。