Vuex の mapXXXX メソッドに型をつけるためのライブラリを作った

## TL;DR

https://github.com/c6h4clch3/vuex-mapper-typed
https://www.npmjs.com/package/vuex-mapper-typed

- Vuex モジュール定義から型定義付きの `mapXXXXWithType` メソッドを導出するライブラリ作ったよ
  - あくまで型情報を付加できるようにしただけで実際やってることは Vuex の `mapXXXX` メソッドを一つのオブジェクトにまとめてるだけ
  - JavaScript で型定義の効力を 100% 受けようと思うと、JSDoc 書く必要あり
- `Vuex.mapXXXX` の返り値への型情報の付け方についてはキャストを利用
  - 今回はモジュール定義由来の型情報をキャストを使って付与する
- 型定義がカオス化しそうなのでオブジェクト形式のマッピングは今回対応を見送り

## 使い方

### インストール

```bash
$ npm install vuex-mapper-typed
```
or

```bash
$ yarn add vuex-mapper-typed
```

### ストアモジュール作成

```ts
// Vuex Module
import { buildModule, makeMappers } from "vuex-mapper-typed";

// モジュール定義
// 返り値は Vuex.Module の拡張のためこのまま Vuex モジュールの定義として扱える
// というか型情報の拡張と初期値としての空オブジェクトの設定以上のことはやってない
export const hogeModule = buildModule({
  namespaced: true,
  state: {
    id: 1,
    name: "John Doe"
  },
  actions: {
    rename({ commit }, name: string) {
      commit("rename", name);
    }
  },
  mutations: {
    updateName(state, name: string) {
      state.name = name;
    }
  }
});
// モジュール定義からマッパーオブジェクト作成
export const hogeMappers = makeMappers(hogeModule);
```

### 利用する

#### ストア定義

```ts
// Vuex Store
import Vue from "vue";
import Vuex from "vuex";
import { hogeModule } from "./path/to/module-definition";

Vue.use(Vuex);

export default new Vuex.Store({
  module: {
    hoge: hogeModule
  }
});
```

#### コンポーネント

```ts
// Vue Component
<script lang="ts">
import Vue from 'vue';
import { hogeMapper } from './path/to/module-definition';

const hogeMappedState = hogeMapper.mapStateWithType(
  'hoge',
  ['id', 'name']
);

export default Vue.extend({
  computed: {
    ...hogeMappedState
  },
  methods: {
    hogehoge() {
      this.id   // (property) this.id: number;
      this.name // (property) this.name: string;
    }
  }
});
</script>
```

マッピングの際にマッパタプル(キー文字列の配列)を与えなければ全てのプロパティを割り当てるようになっています。

![スクリーンショット 2019-09-17 16.23.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208016/d827736d-379e-0170-0f64-99885dd6517a.png)

推論が効く！！

マッパタプルの作成時もちゃんと候補絞り込んでくれます！

## 技術的な詳細

### ざっくりとした全体の流れ

1. モジュール定義の各パーツを型変数に設定した上で、モジュール定義を引数として要求する関数を作成
    - 引数の型情報を型変数に吸い上げる
2. モジュール定義の型情報を使った型パズルを組み上げて、`mapXXXX` の返り値の型を作成
3. `mapXXXX` の返り値を 2. の型情報にキャストするラッパーを提供

という流れ。

### 型パズルに使った文法

#### Static types for dynamically named properties

モジュール定義から mapXXXXWithType に型情報を与えるのに使用。

早い話が `keyof` キーワードと `T[K]` 記法の組み合わせ。

##### keyof

`keyof T` で型 `T` のオブジェクトのキーの一覧の直和型を取得できる。
この直和型は出来る限り Literal Types で詳細に推論される。

```ts
interface User {
  id: number;
  name: string;
};

type KeyOfUser = keyof User; // 'id' | 'name'
```

##### T[K]

`T[K]` (ただし、`K extends keyof T`)で、型 `T` のオブジェクトのプロパティ `K` の型を取得できる。

```ts
type UID = T['id']; // 'number'
```

#### MappedType

 
```ts
{ [P in K]: T }
```

直和型 `K` を構成する要素 `P` に対応する値 `T` の組み合わせで型を構成できる。
`keyof` や `T[K]` と組み合わせれば、動的に型を組み上げる事ができる。
例えば、あるオブジェクト `T` に対してそのゲッタのマッパーを定義したいってときは下記のような定義になる。

```ts
type Mapper<T> = {
  [P in keyof T]: () => T[P];
};
```

#### Conditional types

型情報に応じて動的に型を振り分ける仕組みの一つ。
型演算における三項演算子みたいなもの。
構文は `T extends A ? B : C` で、この場合、
「型 T の値が型 A の変数に代入可能であれば型 B、そうでなければ型 C」になる。
また、A の部分を `infer D` などとすることで、
その部分の型を型変数として利用することが出来る。

```ts
// conditional type
type Value<T> = T extends (infer S)[] ? S : never; // Value<string[]> = string;
```

#### TypeScript の組み込み型

例えば、オブジェクトから特定のキーだけ抜き出す `Pick<T, S>`、
関数の引数をタプルとして抽出する `Parameters<T>` など。

[一覧はこちら](https://github.com/Microsoft/TypeScript/blob/93ab352189245fd3b2751e5ab0ad3ffee4906fca/src/lib/es5.d.ts#L1331-L1394)

### 型パズルの例

[State -> mapState の型パズル](https://github.com/c6h4clch3/vuex-mapper-typed/blob/develop/src/mappers/state.ts)

多分一番読みやすい「`State` の型情報から `mapState` の型情報を導出する」コード。



## 今後の展望

- 「モジュールの定義」と「モジュールがどういう名前空間で使われるのか」の部分が推論に影響しないので、モジュール自身が自分の使われるべき名前空間を規定できるようにする
    - うっかり API 構造を壊しそうなので、慎重に......
- Action 定義のメソッドの第一引数内で推論が反映されていない部分があるので対応したい(getters など)
    - ただし、`Vuex.Action` 自体の構造が自己言及的なので、改善は難しそう

Vue 3.0 に伴う改修で不要になるといいな、と思いますが、是非使ってみて下さい！