react-reduxのconnect()から学ぶTypeScriptの型定義テクニック

こんにちは！ Hanakla(Twitter: [@hanak1a_](https://twitter.com/hanak1a_))です！ TypeScriptアドベントカレンダー 15日目となるこの記事では、
react-reduxの`connect`関数を自前実装して型定義テクニックを学んでいきたいと思います。 みんなもつよつよ型定義を書いてつよつよなライブラリを作っていこう！

みなさん、`""react-redux""`してますか〜!？ してますよね！
じゃあ`react-redux`のAPIも知ってますか〜！？ お世話になってる、なるほど！！
じゃあじゃあ、その型定義を読んだことがある人は〜！？ ある！！？じゃあこの記事読まなくていいな！ 家に帰ってサンタさんからの請求を部屋の隅でガタガタ震えながら待っていてくれ！ﾎｰﾎｰﾎｰ:santa::money_with_wings: :money_with_wings: :money_with_wings: 

## `connect(mapStateToProps, mapDispatchToProps)(Component)`

みんな大好きですね、最近は`useSelector`の繁栄によって書く機会は少ないとおもいますが、100回は書いた人も多いんじゃないでしょうか。 `connect`の型定義については正確には100行くらいの型定義になってしまうので、上記のインターフェースの型定義について解説してみます。（実際の.d.tsの定義に基づいた解説ではありません、自前型定義をします。）

`connect()`は第一引数にstateから状態を取り出す関数、第二引数にpropsに割り当てるactionを指定する関数を渡し、コンポーネントをラップする関数を書きます。ここまでをざっくり書いてみましょう

```ts
declare function connect<StateProps, DispatchProps>(
  mapStateToProps: (state: unknown) => StateProps,
  mapDispatchToProps: () => DispatchProps,
): ConnectedComponentFactory<StateProps & DispatchProps>
```

まず`connect`に2つのジェネリック型を受け付けています。この型は指定されていませんが、以下の型から推論されます。

- `StateProps`は`mapStateToProps`の返り値型
- `DispatchProps`は`mapDispatchToProps`の返り値型

VSCode上で確認すると、正しく推論されていることがわかります。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57508/23ade4b1-65fe-f772-b972-92fef2dd9358.png" />

では次に`ConnectedComponentFactory`（ラップしたコンポーネントを返す関数）の型を書いてみます。 そしてこちらに既に調理されたコードがあります！

```ts
import React from 'react'

type ConnectedComponentFactory<ConnectedProps> =
  <P extends ConnectedProps>(
    Component: React.ComponentType<P>,
  ) => React.ComponentType<P>;
```

`ConnectedProps`には、`mapStateToProps`, `mapDispatchToProps`の返り値を複合した型が入ってきます。
次に`<P extends ConnectedProps>(Component: React.ComponentType<P>) => ...`のジェネリック型がありますが、これは受け取ったコンポーネントのpropsの型を`P`に受け付けます。

```ts
declare const factory: ConnectedComponentFactory
const Component = (props: { hello: string }) => null
factory(Component) // `P`の型は`{ hello: string }`になる
```

> なぜここで`(Component: React.ComponentType<any>) => ...`を使わないかというと、Propsの型が必要になるためです。もし`React.ComponentType<any>`を受け付けてしまうとそこから推論されるPropsの型は`ConnectedProps`のみになってしまいます。 ジェネリクスで`extends なんとか`とすることでより具象的な型を得ることが出来ます。ライブラリの型定義ではありがちなテクニックです。

>
> ```ts
> declare function some(str: string);
> some('hello') // => strはstring型
> 
> declare function some<T extends string>(str: T);
> some('hello') // => strは'hello'型
> ```


最後に`React.ComponentType<P>`で`ConnetedProps`と、元々受け取っているProps(`P`)を合成して全てのPropsの型が出来上がります。(`P extends ConnectedProps`があるので`P`は`ConnectedProps`を内包しています）


ここまでのまとめです

```ts
import React from 'react'

type ConnectedComponentFactory<ConnectedProps> =
  <P extends ConnectedProps>(
    Component: React.ComponentType<P>,
  ) => React.ComponentType<P>;

declare function connect<StateProps, DispatchProps>(
  mapStateToProps: (state: unknown) => MapStateToProps,
  mapDispatchToProps: () => DispatchProps,
): ConnectedComponentFactory<StateProps & DispatchProps>
```

まだ続きます。こうして生成された`React.ComponentType<StateProps & DispatchProps & Props>`型ですが、`conenct`関数は`mapStateToProps`,`mapDispatchToProps`で渡されたPropsを自動的にコンポーネントに与えるため、このコンポーネントの利用側から見える必要がありません。

しかし、全てのPropsの型が「外側から与えられる必要のあるもの」として定義されてしまっています。これでは型がオオカミ少年ですね。なので、connectされたPropsを外部から見えないようにします。

そのためには`ConnectedComponentFactory`の返り値のPropsの型から`ConnectedProps`のキーを取り除きます。

```ts
type ConnectedComponentFactory<ConnectedProps> =
  <P extends ConnectedProps>(
    Component: React.ComponentType<P>,
  ) => React.ComponentType<Pick<P, Exclude<keyof P, keyof ConnectedProps>>>;
```

返り値の型に注目してください。以下のような流れでPからConnectedPropsのキーを除外しています。

- `Exclude<keyof P, keyof ConnectedProps>`  
  `P`のキーから`ConnectedProps`のキーを除外する。 これでコンポーネントが本来必要としているPropsが残る(仮に`OwnPropsKeys`と呼びましょう)
- `Pick<P, OwnPropsKeys>`
  Pの内OwnPropsKeysのキーを持つプロパティだけを抽出する。（これを`OwnProps`と呼びましょう）
- `React.ComponentType<OwnProps>`  
  必要だったプロパティだけが残ったコンポーネント型が生成される。

ではこれでいくつかのコンポーネントを実際に用意してテストしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57508/f6c52f5d-ed25-ba62-954d-5c9b0b392350.png)

- Valid → すべての型定義が一致している。connectされるpropsをcreateElementで省いてもエラーはない。
- Missing → `state` propが欠けている。ちゃんと`ConnectedComponentFactory`の引数に型エラーが出ている。 実は`dispatch`の型定義が間違っているが、これは（本家の型定義でも）エラーになっていない。
- Invalid → `own`の型がnumberになっている（正しくはstring）。ちゃんとエラーになっている。

このような感じで型定義のチェックも通っています。正しそうですね！
`Missing#dispatch`の返り値型が一致していないのですが、これはTypeScriptのジェネリクス型の推論が上手く通らず握りつぶされてしまいます。具体的には以下の箇所で発生しているはずです。

```ts
type ConnectedComponentFactory<ConnectedProps> =
  <P extends ConnectedProps>(
```

`P extends ConnectedProps`によって型の制約がついているはずなのですが、実際の`P`の型が優先されてしまうため、キーさえ一致していればP側の型が採用されてしまいます。ここらへんは @na-o-ys さんの「[なぜ TypeScript の型システムが健全性を諦めているか](https://qiita.com/na-o-ys/items/aa56d678cdf0de2bdd79)」という記事の話と被ってそうなそれ以前の話そうな感じですね…

（公式の型定義でも同様の問題があるので`mapDispatchToProps`はTypeScriptにおいては現状危険です。今日日`this.props.dispatch`や`useDispatch`があるので使う理由がありません。コンポーネントの読み通しも難しくなるので使うのをやめましょう。）

### 最後に

元気があったらもうちょっと色々な型定義の話をしてみんなを（健全な領域の）型パズル沼に嵌めたかったのですが、体力が切れたのでここまでです。

最後に、[Twitterでのフォロー](https://twitter.com/_ragg_)お願いします！ 普段[Fleur](https://github.com/fleur-js/fleur)というFluxフレームワークを作っていて、TypeScript周りの話やフロントエンドの設計周りの話で悩んでいることが多いので、みなさんの設計へのこだわりとかを雑にリプライで教えていただけるとFleurの設計の改善に役立ちます！よろしくお願いします :bow:

最後まで読んで頂きありがとうございました！ 明日は @isy さんによるテストの話です！ お楽しみに！

（React畑の方は僕が書いた `React Hooks Advent Calendar 2019 11日目`の記事 [エーッ！HooksでcomponentDidUpdate出来ないんですか！！？](https://qiita.com/ra_gg/items/f57e689972e2f4438eb6) もぜひ読んでみて下さい！）