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

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

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

 [TypeScript 2.1 で導入される `keyof` を使って `EventEmitter` を定義してみる](http://qiita.com/kimamula/items/ee468b1ed5b3f45dee0f)

## 元のコード

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

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

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

 ```javascript
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名

例えば、こんな感じ ↓

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

## Step By Step

### stateの型付け

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

```ts
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` を片付けしていきます。

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

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

### `mutations` と `actions` の型付け

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

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

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

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

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

`actions` も同じ要領で。

```ts
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` は以下のように定義されています。

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

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

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

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

```ts
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` も同じく。

```ts
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;
```

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

```ts
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` として定義してやれば、
`dispatch` や `commit` の呼び出し時の引数がコンパイラによってチェックされるようになります。

```ts
@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が出てくる時にはできるようになっていると思いますが、とりあえず現段階では、こんな感じでオーバーロードを追加しておけば補完できるはず。

```ts
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[]>;
```

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