こんにちは
nuxt + typescript でフロントエンドを作っているエンジニアです。
store の getter や actions にも type を効かせるときに手を焼いたので共有します。
目次
- ベースの作成
- 公式の見解
- vuex-module-decorators で型付け
- vuex-class-component で型付け
- vuex-class-component での注意点
- まとめ
- axios の設定
ベース作成
nuxt × typescript の構築は他の記事に譲るとし、今回の説明で必要なファイルを列挙します。
なお、今回は shops を中心に store の説明をします。
まずは型定義
export interface Shop {
name: string;
}
そして pages
<template>
<div>
{{ shopOptions }}
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { mapGetters } from 'vuex';
import { Shop } from '~/types';
export default Vue.extend({
computed: {
...mapGetters({
shopOptions: 'shops/values',
}),
},
mounted() {
this.$store.dispatch('shops/fetch');
},
});
</script>
今回問題となっている store
fetch を実行すると、サーバーからファイルを取得し、values に保持します。
clear を実行すると、保持していた values を clear します。
import { Shop } from "~/types";
import { GetterTree } from "vuex/types/index";
import { RootState } from "~/store/types";
export const types = {
SET: 'SET',
CLEAR: 'CLEAR',
};
interface State {
values: Shop[];
}
export const state: () => State = () => ({
values: [],
});
export const mutations = {
[types.SET](state: State, shops: Shop[]): void {
state.values = shops;
},
[types.CLEAR](state: State): void {
state.values = [];
},
};
export const actions = {
async fetch({ commit, getters }): Promise<void> {
if (getters.values.length > 0) {
return;
}
const { data } = await this.$axios.get('/shops');
commit(types.SET, data);
},
clear({ commit }): void {
commit(types.CLEAR);
},
};
export const getters: GetterTree<State, RootState> = {
values(state: State): Shop[] {
return state.values;
}
};
以上の 3ファイルになります。
上記のコードだと、 component 側で typo しても、なんのエラーも出ません。
mounted() {
this.$store.dispatch('shops/fech'); // typo
},
</script>
これを typescript のエラーとして警告してくれるのがゴールです。
公式の見解
Nuxt TypeScript では、以下の選択肢が与えられています
- vanilla (今回は触れません)
- vuex-module-decorators
- vuex-class-component
ググってみると、vuex-module-decorators
がベストのような記事がありますが、
vuex-class-component
もいいよ!みたいな記事もあったので、試してみました。
vuex-module-decorators で型付け
まずは vuex-module-decorators
で型付けをするやり方を見ていきます。
いつも通りライブラリをインストールします。
yarn add vuex-module-decorators
store を直していきます。
axios は後ほどやるので、ひとまずは仮データで進めます。
import {
Module,
VuexModule,
Mutation,
Action,
} from "vuex-module-decorators";
import { Shop } from "~/types";
@Module({
name: 'shops',
stateFactory: true,
namespaced: true,
})
export default class ShopsStore extends VuexModule {
private shops: Shop[] = [];
@Mutation
private SET(shops: Shop[]): void {
this.shops = shops;
}
@Mutation
private CLEAR(): void {
this.shops = [];
}
@Action({})
public async fetch(): Promise<void> {
if (this.shops.length > 0) {
return;
}
const data = [{ name: 'tokyo' }];
this.SET(data);
}
@Action({})
public clear(): void {
this.CLEAR();
}
public get values(): Shop[] {
return this.shops;
}
}
State は内部変数として書きます。private
をつけると直接参照したときに Typescript エラーを起こしてくれます。
getter は public get
のメソッドとして書き直します。
次に Initialize plugin を設定していきます。公式で書かれている内容なのでさらっといきます。
import { Store } from 'vuex';
import { initializeStores } from '~/utils/store-accessor';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const initializer = (store: Store<any>): void => initializeStores(store);
export const plugins = [initializer];
export * from '~/utils/store-accessor';
import { Store } from 'vuex';
import { getModule } from 'vuex-module-decorators';
import ShopsStore from '~/store/shops';
let shopsStore: ShopsStore;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function initializeStores(store: Store<any>): void {
shopsStore = getModule(ShopsStore, store);
}
export { initializeStores, shopsStore };
設定した store を読み込むように pages 側を変更します。
<script lang="ts">
+ import { shopsStore } from '~/store';
(中略)
computed: {
- ...mapGetters({
- shopOptions: 'shops/values',
- }),
+ shopOptions(): Shop[] {
+ return shopsStore.values;
+ },
},
mounted() {
- this.$store.dispatch('shops/fetch');
+ shopsStore.fetch();
},
</script>
ここまでの変更で、typescript error が出るようになります。
<script lang="ts">
(中略)
mounted() {
shopsStore.fech(); // typo
},
</script>
180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'?
> 180 | await shopsStore.fech();
| ^
以上で vuex-module-decorators による型付けは成功しました。
axios がまだ設定できていないので、そちらはおまけでやっていきます。
vuex-class-component で型付け
vuex-class-component
で型付けをするやり方を見ていきます。
ライブラリをインストールします。
yarn add vuex-class-component
store を直していきます。
import {
createModule,
mutation,
action,
createProxy,
extractVuexModule,
} from "vuex-class-component";
import Vuex from 'vuex';
import Vue from 'vue';
import { $axios } from '~/utils/api';
import { Shop } from "~/types";
Vue.use(Vuex);
const VuexModule = createModule({
namespaced: 'shops',
strict: false,
target: 'nuxt',
});
export class ShopsStore extends VuexModule {
private shops: Shop[] = [];
@mutation
private SET(shops: Shop[]): void {
this.shops = shops;
}
@mutation
private CLEAR(): void {
this.shops = [];
}
@action
public async fetch(): Promise<void> {
if (this.shops.length > 0) {
return;
}
const data = [{ name: 'tokyo' }];
this.SET(data);
}
@action
public async clear(): Promise<void> {
this.CLEAR();
}
public get values(): Shop[] {
return this.shops;
}
}
const store = new Vuex.Store({
modules: {
...extractVuexModule(ShopsStore),
}
});
export const shopsStore: ShopsStore = createProxy(store, ShopsStore);
vuex-module-decorators
と似ていますが、Module 指定のや store 登録周りの書き方が違います。
また、vuex-module-decorators
とは違い、initialize plugin にあたる処理を各 store の中でやっています。
設定した store を読み込むように pages 側を変更します。
<script lang="ts">
import { shopsStore } from '~/store/shops';
(中略)
computed: {
- ...mapGetters({
- shopOptions: 'shops/values',
- }),
+ shopOptions(): Shop[] {
+ return shopsStore.values;
+ },
},
mounted() {
- this.$store.dispatch('shops/fetch');
+ shopsStore.fetch();
},
</script>
vuex-module-decorators の時と同じように、typescript error が出るようになります。
<script lang="ts">
(中略)
mounted() {
shopsStore.fech(); // typo
},
</script>
180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'?
> 180 | await shopsStore.fech();
| ^
vuex-class-component での注意点
shopsStore.CreateProxy(~, ~)
このような形で、store.CreateProxy() を呼びましょうと書いてある記事が多かったのですが、これは古い readme に書かれていた内容のようです。(2019/12/12 時点)
まとめ
調べてみる限り vuex-module-decorators
が優勢のようですが、書いてみると vuex-class-component
の方が変更ファイルも少なく、書きやすい印象がありました。
公式は vuex-module-decorators
を推してるけど、vuex-class-component
も悪くないよ! という内容でした。
ご指摘あったらコメントください!
axios の設定
上記のコードでは axios の設定ができていないので、やっていきます。
とはいえ、公式 にも書いてあるので、参考程度に載せておきます。
なお、vuex-module-decorators でも vuex-class-component でも同じ書き方で動きます。
import { NuxtAxiosInstance } from '@nuxtjs/axios';
let $axios: NuxtAxiosInstance;
export function initializeAxios(axiosInstance: NuxtAxiosInstance): void {
$axios = axiosInstance;
}
export { $axios };
import { Plugin } from '@nuxt/types';
import { initializeAxios } from '~/utils/api';
export const accessor: Plugin = ({ $axios }): void => {
initializeAxios($axios);
};
export default accessor;
plugins: [
'@/plugins/axios-accessor',
]
import { $axios } from '~/utils/api';
(中略)
const { data } = await $axios.get('/shops');
以上!