LoginSignup
9
8

More than 5 years have passed since last update.

keyof で Vuex.Storeを型付けする (2016-12-01版)

Posted at

TypeScript 2.2.0が正式にリリースされた時点で Vuex 本体の型定義にも何らかの修正が入る可能性がありますし、情報としての賞味期限は短そうですが、現時点で keyof を Vuex で利用するためのプラクティス的なあれ。

keyof に関する詳細はこちらの記事をどうぞ

TypeScript 2.1 で導入される keyof を使って EventEmitter を定義してみる

元のコード

Vuexの公式ドキュメントに載っているこのコードを材料にします。

const store = new Vuex.Store({
  state: { count: 0 },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

と言いつつ、あまりにもさびしいのでいくらかコードを追加。

const store = new Vuex.Store({
  state: { count: 0, message: "" },
  mutations: {
    increment (state) {
      state.count++;
    },
    setMessage (message) {
      state.message = message;
    }
  },
  actions: {
    increment (context) {
      context.commit('increment');
    },
    hello (context, name) {
      context.commit('setMessage', "Hello, " + name);
    }
    greet (context, { greeting, name }) {
      context.commit('setMessage', greeting + ", " + name);
    }
  }
})

目標

以下のことを、コンパイル時に静的にチェックできるようにする。

  • state のメンバー名と型
  • mutations および actions 内に、必要なメンバーがすべて記述されていること
  • mutations 内での操作で、stateのプロパティ名の間違いや型のミスマッチがないこと
  • コンポーネントから store.dispatch()store.commit() を呼ぶときに、typeの間違いや引数(Payload)の型の間違いがないこと
    • 例えば、 store.dispatch('greeet', "John")store.dispatch('greet', 0) をコンパイルエラーとして検出する

また、Visual Studio Codeなどを使った時に、以下の箇所で入力補完が効くようにする。
* mutations および actions 配下のメンバー名
* コンポーネントから store.dispatch()store.commit() を呼ぶときのtype名(第一引数)
* mutations 配下の実装で state を操作するときのメンバー名
* actions 配下の実装で context.commit() を呼ぶときのtype名

例えば、こんな感じ ↓

  actions: {
    increment (context) {
      context.commit('|/* ここで(increment|setMessage)を補完 */')
    },
    |/* ここで(hello|greet)を補完 */
  }

Step By Step

stateの型付け

Vuexの型定義ファイルに従って普通にTypeScript化するとこんな感じです。
stateが型付けされるので、mutations内でのstate操作で補完やコンパイラチェックが効くようになります。

interface MyState {
  count: number;
  message: stirng;
}

const store = new Vuex.Store<MyState>({
  state: { count: 0, message: "" },
  mutations: {
    increment (state) {
      state.count++;
    },
    setMessage (state, message) {
      state.message = message;
    }
  },
  actions: {
    // 省略
  }
});

Payload の型を定義

個々のmutationとactionの名前と、引数として渡すオブジェクトの型をインターフェイスで定義します。

このインターフェイスを元に、 mutations, actions, dispatch, commit を片付けしていきます。

// 各mutationのpayloadの型
interface MyMutationPayloads {
  increment: undefined;   // 引数なしは undefined とか null で表現
  setMessage: string;
}

// 各actionのpayloadの型
interface MyActionPayloads {
  increment: undefined;
  hello: string;
  greet: { greeting: string, name: string };
}

mutationsactions の型付け

上で定義したインターフェイスを使うと、 mutations の型(MyMutations)は以下のように定義できます。
(ただしもっとシンプルに定義できる方法があるかもしれない)

// 個々のmutationの型
type MyMutationHandler<K extends keyof MyMutationPayloads> = (state: MyState, payload: MyMutationPayloads[K]) => void;
// mutationsの型
type MyMutations = { [K in keyof MyMutationPayloads]: MyMutationHandler<K> };

これで、以下のように定義すれば、 incrementsetMessage の部分は補完が効くし、間違えたり足らなかったりしたら
コンパイラがエラーにしてくれるようになります。

const mutations: MyMutations = {
  increment(state, _) {
    state.count++;
  },
  setMessage(state, message) {
    state.message = message;
  }
}

引数のない mutation は定義できなくなるので increment の方にはダミーの引数を足す必要がありますが、まあささいな問題かと。(MyMutationHandler の定義で payloadpayload? にすれば引数なしにできるようになりますが、逆に引数必須な mutation に対しても引数のないfunctionを突っ込めるようになるので、良し悪しかなあという感じ)

actions も同じ要領で。

type MyActionContext = Vuex.ActionContext<AppState, AppState>;
type MyActionHandler<K extends keyof MyActionPayloads> = (context: MyActionContext, payload: MyActionPayloads[K])
type MyActions<K extends keyof MyActionPayloads> = { [K in keyof MyActionPayloads]: MyActionHandler<K> };

const actions: MyActions = {
  increment(context) {
    context.commit('increment');
  },
  hello(context, name) {
    context.commit('setMessage', "Hello, " + name);
  }
  greet(context, { greeting, name }) {
    context.commit('setMessage', greeting + ", " + name);
  }
}

dispatch と commit の型付け

Vuexの型定義では、dispatch メソッドの型 Dispatch は以下のように定義されています。

export interface Payload {
  type: string;
}
export interface Dispatch {
  (type: string, payload?: any): Promise<any[]>;
  <P extends Payload>(payloadWithType: P): Promise<any[]>;
}

以下のように第一引数にtypeを指定するパターンと、typeをメンバとしてもつオブジェクトを渡すパターンの2通りの使い方ができるわけですね。

dispatch('greet', { greeting: 'Hello', name: 'vue' });
dispatch({ type: 'greet', greeting: 'Hello', name: 'vue' });

この Dispatch の定義と互換性を保ちつつ、MyActionPayloads を使ってより厳密に型付けした MyDispatch 型を定義します。

type ActionPayloadWithType<K extends keyof MyActionPayloads, V extends MyActionPayloads[K]> = { type: K } & V;

interface MyDispatch {
    <K extends keyof MyActionPayloads>(type: K, payload: MyActionPayloads[K]): Promise<any[]>;
    <K extends keyof MyActionPayloads, V extends MyActionPayloads[K]>(payloadWithType: ActionPayloadWithType<K, V>): Promise<any[]>;
}

commit も同じく。

type MutationPayloadWithType<K extends keyof MyMutationPayloads, V extends MyMutationPayloads[K]> = { type: K } & V;

interface MyCommit {
    <K extends keyof MyMutationPayloads>(type: K, payload: MyMutationPayloads[K], options?: Vuex.CommitOptions): void;
    <K extends keyof MyMutationPayloads, V extends MyMutationPayloads[K]>(payloadWithType: MutationPayloadWithType<K, V>, options?: Vuex.CommitOptions): void;

dispatchcommit を持つのはStoreとActionContextなので、それらの型を再定義。

interface MyStore extends Vuex.Store<MyState> {
  dispatch: MyDispatch;
  commit: MyCommit;
}

interface MyActionContext extends Vuex.ActionContext<MyState, MyState> {
  dispatch: MyDispatch;
  commit: MyCommit;
}

あとは、コンポーネントを vue-class-component とかで定義するときに $store の型を MyStore として定義してやれば、
dispatchcommit の呼び出し時の引数がコンパイラによってチェックされるようになります。

@component
class MyComponent {
  $store: MyStore;

  onClick(e) {
    // 'greet' の部分でtypoしてたり、引数の型が合わなかったりしたらコンパイルエラーになる
    this.$store.dispatch('greet', { greeting: "こんにちは", name: "vue" });
  }
}

action の中から context.commit()context.dispatch() を呼ぶ場合も同様。

※ただし、action名/mutation名の部分については、現状補完が効きません。これについてissueをあげたら2.2.0のマイルストーンに入ったので、2.2.0が出てくる時にはできるようになっていると思いますが、とりあえず現段階では、こんな感じでオーバーロードを追加しておけば補完できるはず。

interface MyDispatch {
    (type: keyof MyActionPayloads, payload: never); // ← 追加
    <K extends keyof MyActionPayloads>(type: K, payload: MyActionPayloads[K]): Promise<any[]>;
    <K extends keyof MyActionPayloads, V extends MyActionPayloads[K]>(payloadWithType: ActionPayloadWithType<K, V>): Promise<any[]>;

というところでひとまずおしまい。

9
8
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
9
8