Edited at

TypeScriptでVuexを使うときのスタンダードっぽいやりかたを調べてみた

この記事は :christmas_tree:オープンストリーム Advent Calendar 2018:christmas_tree: の11日目の記事です。

うっかり期限を勘違いしていて前日に慌てて記事を書いています!間に合え!!


やりたいこと


  • 最近やっと TypeScriptVue.js を使い始めたので Vuex も一緒に使ってみたい。

  • とりあえず vue-cli でプロジェクトを作成すると、 store.ts ってファイルがぺろっと一つ追加される。けど、できれば1ファイルにべた書きするんじゃなくて、いいかんじに分けて書きたい。けど何をどう書けばいいのかよくわからない:thinking:


    • あとたぶん別の言語やって1ヶ月もしたら内容をすっぱり忘れるので、備忘録もほしい:innocent:



というわけで、調べてみた。


結論

ここにぜんぶ載ってた。すてき。

https://codeburst.io/vuex-and-typescript-3427ba78cfa8

……なのですが。これだけだと、ちょっと弊社の偉い人に怒られてしm記事としてアレなので、もうちょい噛み砕いた内容で書いていきます。


注意

2018/12/10 21:00 現在、Vue.jsの最新版にバグがあるようで、掲載した内容について動作確認まで行えていません(一応バグのある箇所以外はコンパイルエラーにならないことだけは確認してますが)。動作確認できてから改めてソースコードなど手直しを行います…

https://github.com/vuejs/vue/pull/9173


本題


ファイル構成

vue-cli create hoge で作成されたテンプレからHelloWorld.vue をひっこぬいて Todo.vue と store 以下を足したような状態です。(src 以下の関係あるもののみ抜粋)

src

├── App.vue
├── components
│ └── Todo.vue
├── main.ts
└── store
├── Todo
│ ├── actions.ts
│ ├── getters.ts
│ ├── index.ts
│ └── mutations.ts
├── index.ts
└── types.ts

src/store/TodoVuex のモジュールで、 src/store がルートステートになります。

モジュールについては公式のドキュメントをどうぞ。 https://vuex.vuejs.org/ja/guide/modules.html


TodoModule

TODOのリストを管理するやつをイメージしています。

実際はもっとたくさんあれこれ処理があるはずですが、サンプルなので以下省略。


types.ts

モジュール定義の前に、そちらで参照する interface の定義です。

export interface RootState {

version: string;
}

export interface TodosState {
todos: Todo[];
}

export interface Todo {
// 一意になるかんじのID
id: string;
// チェックボックスON/OFF
done: boolean;
// やること
text: string;
}

特になんの変哲もない interface ですね。

RootStateTodosState が今回の state の定義になります。


Module(index.ts)


store/Todo/index.ts

import { Module } from 'vuex';

import { TodosState, RootState } from '@/store/types';
import getters from './getters';
import actions from './actions';
import mutations from './mutations';

const state: TodosState = {
todos: [],
};

export const todos: Module<TodosState, RootState> = {
namespaced: true,
state,
getters,
actions,
mutations,
};


Module のオブジェクトを用意しています。以上。

getters, actions, mutations の定義は続けて以下に記載します。namespaced: true にしておくと、このあとコンポーネントから参照するときにらくちんです。


Actions


store/Todo/actions.ts

import { ActionTree } from 'vuex';

import { TodosState, RootState, Todo } from '@/store/types';

const actions: ActionTree<TodosState, RootState> = {
add: async ({ commit }, todo: Todo) => {
if (await someAsyncAddMethod(todo)) {
commit('add', todo);
// 成功
return true;
}
// 失敗
return false;
},

remove: async ({ commit }, id: string ) => {
if (await someAsyncRemoveMethod(id)) {
commit('remove', id);
return true;
}
return false;
},

};

export default actions;


ActionTree を実装したオブジェクトを用意します。それだけ。

総称型の指定は ActionTree<モジュールのState, ルートのState> です。

引数の { commit } については、他にも定義がありますが、このへんは公式ドキュメントのままですのでそちらを参照してください。

mutation を直接呼び出すか、必ず action を介するかは好みでいいんじゃないかなーと思いますが、サーバのAPIを叩くなど非同期処理を伴う場合は必ず action 経由で行いましょう。mutation で非同期処理はダメ、絶対。


Getters


store/Todo/getters.ts

import { GetterTree } from 'vuex';

import { TodosState, RootState } from '@/store/types';

const getters: GetterTree<TodosState, RootState> = {
size: (state: TodosState) => {
return state.todos.length;
},
};

export default getters;


とりあえず感がすごい。よい例が思いつかずすみません。

ここではGetterTreeのオブジェクトを用意します。引数については action と同様です。

state の内容を加工して参照する必要があるものはgetterを介するのかなーと思いますが、自分の場合、単純にそのまま参照して問題ないなら state の内容を直接参照しちゃっても問題ないんじゃないかな派です。


Mutations


store/Todo/mutations.ts

import { MutationTree } from 'vuex';

import { TodosState, Todo } from '@/store/types';

const mutations: MutationTree<TodosState> = {
add: (state, todo: Todo) => {
state.todos.push(todo);
},
remove: (state, id: string) => {
state.todos = state.todos.filter((e: Todo) => e.id !== id);
},
};

export default mutations;


state を編集する役割を担うもの。これはあんまり迷わなくていいですね。

1つ気になってるのは state.todos.push(todo); の行。オブジェクトに対する編集になるのか、配列に対する編集扱いになるのかがまだちょっとはっきりしません。(このへんは動作確認したら追記します)


RootState

ルートの定義はこんなかんじ


store/index.ts

import Vuex, { StoreOptions } from 'vuex';

import { RootState } from './types';
import { todos } from './Todo';

const store: StoreOptions<RootState> = {
state: {
version: '1.0.0',
},
modules: {
todos,
},
};

export default new Vuex.Store<RootState>(store);


import { todos } from './Todo'; で、上記でつくった TodosState をインポートしています。

さらにそこからルートステートの modules に設定しています。


コンポーネントからの参照


components/todo.vue

// @Component

export default class TodoComponent extends Vue {

@TodoModule.State('todos')
private todos!: Todo[];

@TodoModule.Getter('size')
private size!: number;

@TodoModule.Action('add')
private add!: (todo: Todo) => Promise<boolean>;

private inputText: string = '';

private addTodo(): void {
// validate
if (!this.inputText) {
window.alert('なにか入力してね!');
return;
}
this.add({
id: new Date().getMilliseconds().toString(),
// チェックボックスON/OFF
done: false,
// やること
text: this.inputText,
});
}
}


コンポーネントなどから呼び出すときは、最初の記事にあったように vue-class を使うのが良さげです。

ただ、現在は最初の記事の内容から比べるといくらか書き方が変わっているようですので、詳細は公式のドキュメントを確認してください。

コードを全部書くと記事が長くなるのでだいぶ端折っていますが、基本的には@Action, @Mutation, @Getter のアノテーションでプロパティのようにぽんぽん定義していくだけ。モジュール側を参照するときは namespace('moduleName') を経由するだけ。簡単ですね!


まとめ

最初の記事のおかげでTypeScriptでのVuexの使い方がなんとなくつかめたので、助かりました。

あと、迷ったらとりあえず 型定義ファイル を見るというくせもつきました。以前と比べるとだいぶさくっとVue.jsアプリが作れるようになってきたので、あとは忘れないようにしないと…:joy: