Vuex 使うとコード補完が効かなくなりがちです。
ということで可能な限り補完を効かせる挑戦をしてみましょう。
ちなみに、Module まで考え始めると詰みます。これはモジュールに対応していない記事になります。
TypeScript のコンソールアプリに Vue と Vuex を追加して以下のようなコードを書きました。
こういうの試すときにコンソールアプリでいけるのは、まじで神ってる。
import Vuex from 'vuex';
import Vue from 'vue';
Vue.use(Vuex);
// 何も考えずに書いた
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
countup(state, payload: number) {
state.count += payload;
}
},
getters: {
square(state) {
return state.count * state.count;
}
},
actions: {
execute({ commit }, payload: { count: number; amount: number; }) {
let count = 0;
const handle = setInterval(() => {
if (count < payload.count) {
count++;
commit('countup', payload.amount);
} else {
clearInterval(handle);
}
}, 1000);
},
},
});
// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});
// ディスパッチ
store.dispatch('execute', { count: 3, amount: 10 });
// もしくは↓
// store.dispatch({
// type: 'execute',
// count: 3,
// amount: 10,
// });
まず、dispatch と commit と getters でタイプセーフじゃないのが嫌ですね。本当に嫌!
ということで、世の中でタイプセーフにするにはどうしてるのか探してみたら 4 年前の記事になるのですが、以下の記事を見つけました。
これでいいのでは?という気持ちもありますが Store の型定義を変えてるのが気になりました。
いや、まぁそれでいいんですが、じゃぁ Store の型定義を書き換えないとどうなる??というのを試したくなったので以下のようにやってみました。
import { ActionContext } from 'vuex';
export type TypedMutationTree<TMutations, TState> = {
[key in keyof TMutations]: (state: TState, payload: TMutations[key]) => void;
}
export type TypedActionTree<TActions, TState> = {
[key in keyof TActions]: (context: ActionContext<TState, TState>, payload: TActions[key]) => void;
}
interface PayloadWityTypeGeneratorType<T> {
<K extends keyof T, P extends T[K]>(type: K, payload: P): { type: K; } & P;
}
class PayloadWithTypeGenerator<T> {
create: PayloadWityTypeGeneratorType<T> = (type, payload) => {
return { type, ...payload };
}
}
export function createPayloadWithTypeGenerator<T>(): PayloadWityTypeGeneratorType<T> {
return new PayloadWithTypeGenerator<T>().create;
}
これを使うと、こんな感じになる。
import Vuex, { ActionContext, GetterTree } from 'vuex';
import Vue from 'vue';
import * as TypesafeVuex from './typesafe-vuex-generator';
Vue.use(Vuex);
// State の型定義
interface MyState {
count: number;
}
// Mutations の型定義みたいな役割
type MyMutations = {
// name: type of payload
countup: { amount: number }; // mutation の payload はオブジェクトである必要がある(number とかはだめ)
}
// Actions の型定義みたいな役割
type MyActions = {
// name: type of payload
execute: { count: number, amount: number }; // action の payload はオブジェクトである必要がある(number とかはだめ)
}
// TypedMutationTree から作ると型をちゃんと認識してもらえる
const mutations: TypesafeVuex.TypedMutationTree<MyMutations, MyState> = {
countup(state, payload) {
state.count += payload.amount;
}
};
// commit と dispatch に渡す payloadWityType を生成する人
const action = TypesafeVuex.createPayloadWithTypeGenerator<MyActions>();
const mutation = TypesafeVuex.createPayloadWithTypeGenerator<MyMutations>();
const actions: TypesafeVuex.TypedActionTree<MyActions, MyState> = {
execute({ commit }, payload) {
let count = 0;
const handle = setInterval(() => {
if (count < payload.count) {
count++;
// commit の引数は上で定義した mutation を使うと補完が効く
commit(mutation("countup", { amount: payload.amount }));
} else {
clearInterval(handle);
}
}, 1000);
},
};
const getters: GetterTree<MyState, MyState> = {
square(state) {
return state.count * state.count;
}
};
const store = new Vuex.Store<MyState>({
state: {
count: 0,
},
mutations,
getters,
actions,
});
// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
// getters は any のまま…
console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});
// ディスパッチの引数は上で定義した action で作ると補完が効く
store.dispatch(action("execute", { count: 3, amount: 10 }));
イケてない制約として Mutation の payload に number とかのプリミティブ型を使えないというのがあります…。う~ん。
元記事の人のように、Store の型自体をいじって commit や dispatch のシグネチャを変えないとこれ以上は無理っぽい?
ということで元記事の人のコードを参考に、もうちょっとだけ汎用的に使えるように typesafe-vuex.ts というものを作ってみました。
import Vuex, { ActionContext, MutationTree, ActionTree, GetterTree, CommitOptions, StoreOptions } from 'vuex';
export type TypedMutationTree<TState, TMutations> = {
[key in keyof TMutations]: (state: TState, payload: TMutations[key]) => void;
}
export type TypedActionTree<TState, TMutations, TActions> = {
[key in keyof TActions]: (context: TypesafeActionContext<TState, TActions, TMutations>, payload: TActions[key]) => void;
}
export type TypedGetterTree<TState, TGetters> = {
[key in keyof TGetters]: (state: TState) => TGetters[key];
}
export type PayloadWithType<T, K extends keyof T> = {
type: K
} & T
interface TypesafeDispatch<TActions> {
<K extends keyof TActions>(type: K, payload: TActions[K]): Promise<any>;
<K extends keyof TActions>(payloadWithType: PayloadWithType<TActions, K>): Promise<any>;
}
interface TypesafeCommit<TMutations> {
<K extends keyof TMutations>(type: K, payload: TMutations[K], options?: CommitOptions): void;
<K extends keyof TMutations>(payloadWithType: PayloadWithType<TMutations, K>, options?: CommitOptions): void;
}
interface TypesafeActionContext<TState, TActions, TMutations> extends ActionContext<TState, TState> {
dispatch: TypesafeDispatch<TActions>;
commit: TypesafeCommit<TMutations>;
}
export declare class TypesafeVuexStore<TState, TMutations, TActions, TGetters> extends Vuex.Store<TState> {
mutations?: TypedMutationTree<TState, TMutations>;
actions?: TypedActionTree<TState, TMutations, TActions>;
readonly getters: TGetters;
dispatch: TypesafeDispatch<TActions>;
commit: TypesafeCommit<TMutations>;
}
これを使うと以下のように書けます。補完つきで。
import Vuex from 'vuex';
import Vue from 'vue';
import * as TypesafeVuex from './typesafe-vuex';
Vue.use(Vuex);
// State の型定義
interface MyState {
count: number;
}
// mutations の型定義みたいな役割
type MyMutations = {
// name: type of payload
countup: number;
}
// actions の型定義みたいな役割
type MyActions = {
// name: type of payload
execute: { count: number, amount: number };
}
// getters の型定義みたいな役割
type MyGetters = {
square: number;
}
// 実際に使う Store の型
type MyStore = TypesafeVuex.TypesafeVuexStore<MyState, MyMutations, MyActions, MyGetters>;
// TypedMutationTree から作ると型をちゃんと認識してもらえる
const mutations: TypesafeVuex.TypedMutationTree<MyState, MyMutations> = {
countup(state, payload) {
state.count += payload;
}
};
const actions: TypesafeVuex.TypedActionTree<MyState, MyMutations, MyActions> = {
execute({ commit }, payload) {
let count = 0;
const handle = setInterval(() => {
if (count < payload.count) {
count++;
// commit も補完が効く
commit("countup", payload.amount );
} else {
clearInterval(handle);
}
}, 1000);
},
};
const getters: TypesafeVuex.TypedGetterTree<MyState, MyGetters> = {
square(state) {
return state.count * state.count;
}
};
const store: MyStore = new Vuex.Store<MyState>({
state: {
count: 0,
},
mutations,
getters,
actions,
});
// 値が変わった時の処理を追加しておく(じゃないと何も表示されないので)
store.watch(x => x.count, (value, oldValue) => {
// getters も type safe
console.log(`The value changed to ${value} from ${oldValue}. double is ${store.getters.square}`);
});
// ディスパッチも補完が効く
store.dispatch("execute", { count: 3, amount: 10 });
モジュールに対応しようとすると、もうひと頑張りいりそうだけど、これと同じ要領でいけるかな???
あと、mapGetters とかを使った時の補完はあきらめた。
まとめ
頭の体操みたいで楽しいですね。
というか、もっといい感じにやる方法があれば知りたい。