LoginSignup
13
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-30

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

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

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

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

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

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;
}

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

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 というものを作ってみました。

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>;
}

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

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

まとめ

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

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

13
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9