React, Reduxのお型付け

React, ReduxのTypeScriptでの型付けの個人的ベストプラクティスです。
勉強会で発表したので簡単にブログにまとめます。

はじめに

スライドは以下のページにあります。

【参考】React, Reduxのお型付け

コンセプト

型の付け方には様々な方法があると思いますが、今回は以下のようなコンセプトで進めていきます。

  • 楽に
  • 安全に
  • 外部ライブラリに頼らずに

「楽に」というのは、型の記述量を減らすという意味です。
今実際開発しているプロダクトでは、一つのactionを追加するたびに、型を明示するために追記しないといけない部分が複数箇所にわたってしまっていました。
今回は、それを改善するためにいろいろ工夫したものを紹介します。

外部ライブラリというのは、typescript-fsaやtypesafe-actionsやredux-actionなどのことですが、メンテナンスなどの事も考えて、今回は何も使わずに型を付ける方法を紹介します。

実はハンズオンです

コードは以下です。

【参考】mrsekut/react-redux-with-typescript-handson

とても小さいReduxプロダクトを用意しました。
「+」と「-」があって数字を増やすだけのものです。

「all-any-type」というブランチがあります。
これはすでに動く状態ですが、全てany型になっています。
これを一緒に型安全なものにしていきます。

準備

「all-any-type」ブランチを指定してクローンしてください。

$ git clone -b all-any-type https://github.com/mrsekut/react-redux-with-typescript-handson.git

$ cd react-redux-with-typescript-handson

reactなどの依存パッケージをインストールします。
$ npm i

TypeScriptを監視状態でコンパイルします。
$ npm run tsc

別のターミナルで、以下を実行するとブラウザで動きを確認します。
型をつけるだけなので最初から最後まで目に見える変化はありません。
$ npm start

TypeScriptのキホン

軽くTypeScriptの型の基本の話をします。
必要のない方は飛ばしてください。

とりあえずこれさえ知っておけば大丈夫

TypeScriptの型システムの話は、潜り込むとどこまでも深くて大変ですが、まずは以下の4種類ほど知っておけば耐えます。

  • number: 42とか
  • string: “hoge”とか
  • boolean: true, falseとか
  • any
    • なんでもいけるやつ

数値は整数や浮動小数点数などの区別はなく、同じnumberで型を付けます。

anyは万能型で、何にでも対応できる型です。
ただし、これだけだと型のパワーの恩恵を受けられなくなるので、使うのは極力避けたいです。

関数の型の書き方

JSはすべてオブジェクトなこともあり、関数の型の書き方はいくつかあるのですが、今回は以下の形に統一します。

(任意の引数名: 引数の型) => 戻り値の型

型を自作する

TypeScriptで型を自作するには2つの方法があります。
微妙な差はありますが、まずは気にしなくても大丈夫です。

“type”で型に別名を付ける

“interface”でクラスやオブジェクトの仕様を決める

ちなみにVSCodeでは

VSCodeでは、変数はhoverするとその型を確認できますが、typeで作った型では中身を見れるのでちょっと便利です。
Gyazo

interfaceでは見れません。
Gyazo

typeとinterfaceの違いが知りたい方は以下の記事などを参考にしてみてください。

【参考】

では、型付けしていきましょう

ここから実際に、これに型を付けていきます。
実際に型を付け終わったものは別のブランチにあります。

Presentational Componentに型を付ける

Presentational Componentというのは、Reduxと接続していない小さなコンポーネントたちのことを指します。
プロダクトの大半がこのコンポーネントになります。

Counter/index.tsx

src/components/Counter/index.tsx

数値を表示するだけのコンポーネントです。
以下のように型を付けます。

Gyazo

@types/reactとして用意されているReact.FC<T>型を使います。
FCはFunctional Componentの略ですね。

Tの部分にはプロパティを定義した自作の型をはめ込みます。
ここでは、親から受け取るnumber型のnumを書いています。

propsは基本的に書き換えることはないので、readonlyで縛ることでより頑強になります。

Readonly<T>というのはTypeScriptに用意されている型で、Tのプロパティをすべてreadonlyにした型をつくります。

つまり、以下のように全プロパティに「readonly」と書いても同じです。

ですが、全部に全部「readonly」と書くのも面倒なので、Readonly<T>で囲うことで少し楽ができます。
ホバーすると全く同じ様に型が当たっているのがわかるかと思います。

簡単ですね。
以上のようにしてCounterコンポーネントに型が付きました。
ホバーすると型が適用されているのを確認できます。

この調子で型付けをしていきます。

Button/index.tsx

src/components/Button/index.tsx

その名の通り、ボタンのためのコンポーネントです。
React.FC<T>を使うなど、さきほどとだいたい同じです。Gyazo

違う部分は任意の関数を使っている点と、childrenを使っている点です。

ButtonPropsの中の「onClick?」の疑問符は任意のプロパティであることを示します。
このButtonコンポーネントを使う時にonClick属性はあってもなくても良いということです。

@types/reactで用意されているカタガタ

@types/reactには、似たような型がいくつかありますが、一部紹介します。。

  • React.ReactElement
    • divやpのような仮想DOMを表す
    • HTMLElementのReact版のようなもの
  • React.ReactChild
    • ReactElementもしくはstringもしくはnumberを表す
  • React.ReactNode
    • ReactElement, Fragment, Portals, primitiveな型

いろいろありますが、childrenに対しては、React.ReactChildを使っておけば問題なさそうです。

型を付けて何が嬉しいのか

閑話休題。
そもそもの話ですが、Reactを開発する上でコンポーネントに型を付けて何が嬉しいのかについてです。

共同開発をするときや、外部ライブラリとして使うコンポーネントがあるときに、型があることでそのコンポーネントの作者の意図と反した使い方をするのを防ぐことができます。

いまさっき作ったButtonコンポーネントの仕様はButtonPropsで定義しましたが、これと異なる使われ方をするとコンパイルエラーで知らせてくれます。Gyazo

Reduxに型を付ける

では、次にRedux側に型を付けていきます。
今回はDucksデザインパターンを採用しており、actionやreducerはmodule.tsという1つのファイルの中に定義しています。

actionに型を付ける

src/modules/module.ts

ここで用意するactionは「+」「-」各ボタンを押したときに実行されるものです。
asはキャストです。

Gyazo

予め作っておいたActionTypesでキャストすることで型が付きます。

この書き方をすることで、わざわざactionを書くたびにそれようの型を書かなくて済みます。

今までは以下のように書いていました。
一つのaction一つのinterfaceを作っていたのでとても冗長になってしまっていました。

module全体のactionに型を付ける

src/modules/module.ts

あとでreducerに渡すためにmodule全体のactionに型を付けます。Gyazo

ここではTypeScriptのちょっとテクった書き方をしています。

typeof hogeはhogeの型を表します。
ここではactionそれぞれの関数の型になります。

ReturnTypes<T>もTypeScriptが用意しているもので、Tが関数の型の場合、その戻り値の型になります。

ReturnTypes<T>自体はconditional typesを使って以下のように定義されています。

パイプ「|」はUnion typesです。

global stateに型を付ける

src/modules/module.ts

global stateの型を付けます。
stateの初期値の宣言などでも使います。

これもpropsのときと同じ様にreadonlyを付けています。Gyazo

Reducerに型を付ける

src/modules/module.tsGyazo

reduxが用意しているReducer<S, A>型と先ほど定義したmoduleのactionと、global stateの型を使います。

もう一点工夫している箇所が、上記のコードの最後のdefaultの部分でnever型を使っている点です。

neverにはnever型の値しか入りません。
コレを使って、union typesで定義したMainAction型に対して、switch文のcaseの漏れを防ぐことができます。

例として、今回のコードの一つの分岐をコメントアウトすると、コンパイルエラーになるのがわかります。

今回の例では、actionは2つしかないので、漏れが出ることはないと思いますが、プロダクトが大きくなってくるとこの分岐が増えていきます。
最初にreducerを作る時点でこの一行を書いておくことで、actionが増えてきてもうっかり書き忘れることを防ぐことができます。

【参考】

Storeに型をつける

src/store.ts

store自体に型をつけるわけではないですが、各moduleで定義した型をここでまとめます。Gyazo

一番下の行のActionはreduxで用意されている型です。

Container Componentに型をつける

src/containers/index.tsx

これで最後です。
container componentというのはReduxと接続しているコンポーネントのことです。

まずはstateやactionをpropsに変換する関数に型を付けます。
Dispatch<T>はreduxで用意されている型です。
Gyazo

 

次に、containerのpropsに型を付けます。
上記2つの関数のReturnTypesを使います。Gyazo

 

この型をReact.FC<T>を使ってコンポーネントに当てます。Gyazo

おわり

お疲れ様でした。
これでプロダクト全体に型が行き渡り、再び息を吹き返しました。
こんな感じで型を当てていくと、楽に、安全に、当てられるのではないでしょうか。

参考