LoginSignup
9
8

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-05-06

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の場合

Home.vue
export default Vue.extend({
  name: "home",
  components: {
    ProductList
  }
 ... 
})

.vueファイルの、export default { ... }の部分を、Vue.extendに書き換える方法です。

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

vue-class-componentの場合

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の値に型をつけてみる

今回使用するデータ

{
products: [{
    id: 1000;
    name: "T-shirts";
    stock: 100;
    price: 1000;
  },
   {
    id: 1000;
    name: "T-shirts";
    ....
  }]
}

簡単な商品一覧を想定して、productsというプロパティの中に、各商品ごとの情報を配列で格納しています。
これらのidやpriceに対して、静的な型を導入します。

interfaceの定義

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側

@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}にしています。

コンポーネント側

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>
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()がエディタの補完機能でサジェストされる他、格納されているオブジェクトの中身も確認することができます。

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8