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の場合
export default Vue.extend({
name: "home",
components: {
ProductList
}
...
})
.vueファイルの、export default { ... }
の部分を、Vue.extend
に書き換える方法です。
気軽に導入できるメリットがありますが、Vuexを取り入れようとすると、エラーの解消がしづらく、特に$store周りの調整で、意図通りの挙動にならず、詰まることが多い印象でした。
vue-class-componentの場合
@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つのオブジェクトが持っているkeyに型を定義する
- 配列の親要素となるオブジェクトに、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}
にしています。
コンポーネント側
<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>
<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()がエディタの補完機能でサジェストされる他、格納されているオブジェクトの中身も確認することができます。