Vue.js + TypeScript で TypeSafe に Vuex したいんじゃぁ…

Vuex 使うとコード補完が効かなくなりがちです。
ということで可能な限り補完を効かせる挑戦をしてみましょう。

ちなみに、Module まで考え始めると詰みます。これはモジュールに対応していない記事になります。

TypeScript のコンソールアプリに Vue と Vuex を追加して以下のようなコードを書きました。
こういうの試すときにコンソールアプリでいけるのは、まじで神ってる。

```typescript:index.ts
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 年前の記事になるのですが、以下の記事を見つけました。

https://qiita.com/wonderful_panda/items/752793cd1a8de0150610

これでいいのでは？という気持ちもありますが Store の型定義を変えてるのが気になりました。
いや、まぁそれでいいんですが、じゃぁ Store の型定義を書き換えないとどうなる？？というのを試したくなったので以下のようにやってみました。

```typescript:typesafe-vuex-generator.ts
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;
}
```

これを使うと、こんな感じになる。

```typescript:index.ts
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 というものを作ってみました。

```typescript: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>;
}
```

これを使うと以下のように書けます。補完つきで。

```typescript:index.ts
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 とかを使った時の補完はあきらめた。

## まとめ

頭の体操みたいで楽しいですね。

というか、もっといい感じにやる方法があれば知りたい。