ブログを消そうかと思うので、一部の記事をQiitaへ移行してきました。
2019/2/21に投稿したものです。
React, ReduxのTypeScriptでの型付けの個人的ベストプラクティスです。
勉強会で発表したので簡単にブログにまとめます。
はじめに
スライドは以下のページにあります。
コンセプト
型の付け方には様々な方法があると思いますが、今回は以下のようなコンセプトで進めていきます。
- 楽に
- 安全に
- 外部ライブラリに頼らずに
「楽に」というのは、型の記述量を減らすという意味です。
今実際開発しているプロダクトでは、一つの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はすべてオブジェクトなこともあり、関数の型の書き方はいくつかあるのですが、今回は以下の形に統一します。
(任意の引数名: 引数の型) => 戻り値の型
// number型の引数を一つ取って、戻り値のない関数
addFunc: (num: number) => void;
型を自作する
TypeScriptで型を自作するには2つの方法があります。
微妙な差はありますが、まずは気にしなくても大丈夫です。
“type”で型に別名を付ける
// name, ageを持ったオブジェクトに「Person」と命名
type Person = {
name: string;
age: number;
};
“interface”でクラスやオブジェクトの仕様を決める
interface Person {
name: string;
age: number;
}
ちなみにVSCodeでは
VSCodeでは、変数はhoverするとその型を確認できますが、typeで作った型では中身を見れるのでちょっと便利です。
typeとinterfaceの違いが知りたい方は以下の記事などを参考にしてみてください。
【参考】
- Interface vs Type alias in TypeScript 2.7 – Martin Hochel – Medium
- TypeScriptのInterfaceとType aliasの比較 – Qiita
では、型付けしていきましょう
ここから実際に、これに型を付けていきます。
実際に型を付け終わったものは別のブランチにあります。
Presentational Componentに型を付ける
Presentational Componentというのは、Reduxと接続していない小さなコンポーネントたちのことを指します。
プロダクトの大半がこのコンポーネントになります。
Counter/index.tsx
src/components/Counter/index.tsx
数値を表示するだけのコンポーネントです。
以下のように型を付けます。
@types/reactとして用意されているReact.FC<T>
型を使います。
FCはFunctional Componentの略ですね。
T
の部分にはプロパティを定義した自作の型をはめ込みます。
ここでは、親から受け取るnumber型のnumを書いています。
propsは基本的に書き換えることはないので、readonlyで縛ることでより頑強になります。
Readonly<T>
というのはTypeScriptに用意されている型で、Tのプロパティをすべてreadonlyにした型をつくります。
つまり、以下のように全プロパティに「readonly」と書いても同じです。
ですが、全部に全部「readonly」と書くのも面倒なので、Readonlyで囲うことで少し楽ができます。
ホバーすると全く同じ様に型が当たっているのがわかるかと思います。
簡単ですね。
以上のようにしてCounterコンポーネントに型が付きました。
ホバーすると型が適用されているのを確認できます。
この調子で型付けをしていきます。
Button/index.tsx
src/components/Button/index.tsx
その名の通り、ボタンのためのコンポーネントです。
React.FCを使うなど、さきほどとだいたい同じです。
違う部分は任意の関数を使っている点と、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で定義しましたが、これと異なる使われ方をするとコンパイルエラーで知らせてくれます。
Reduxに型を付ける
では、次にRedux側に型を付けていきます。
今回はDucksデザインパターンを採用しており、actionやreducerはmodule.tsという1つのファイルの中に定義しています。
actionに型を付ける
ここで用意するactionは「+」「-」各ボタンを押したときに実行されるものです。
asはキャストです。
予め作っておいたActionTypesでキャストすることで型が付きます。
この書き方をすることで、わざわざactionを書くたびにそれようの型を書かなくて済みます。
今までは以下のように書いていました。
一つのaction一つのinterfaceを作っていたのでとても冗長になってしまっていました。
module全体のactionに型を付ける
あとでreducerに渡すためにmodule全体のactionに型を付けます。
ここではTypeScriptのちょっとテクった書き方をしています。
typeof hoge
はhogeの型を表します。
ここではactionそれぞれの関数の型になります。
ReturnTypes<T>
もTypeScriptが用意しているもので、Tが関数の型の場合、その戻り値の型になります。
ReturnTypes自体はconditional typesを使って以下のように定義されています。
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
パイプ「|」はUnion typesです。
global stateに型を付ける
global stateの型を付けます。
stateの初期値の宣言などでも使います。
これもpropsのときと同じ様にreadonlyを付けています。
Reducerに型を付ける
reduxが用意しているReducer<S, A>
型と先ほど定義したmoduleのactionと、global stateの型を使います。
もう一点工夫している箇所が、上記のコードの最後のdefaultの部分でnever型を使っている点です。
never
にはnever
型の値しか入りません。
コレを使って、union typesで定義したMainAction型に対して、switch文のcaseの漏れを防ぐことができます。
例として、今回のコードの一つの分岐をコメントアウトすると、コンパイルエラーになるのがわかります。
今回の例では、actionは2つしかないので、漏れが出ることはないと思いますが、プロダクトが大きくなってくるとこの分岐が増えていきます。
最初にreducerを作る時点でこの一行を書いておくことで、actionが増えてきてもうっかり書き忘れることを防ぐことができます。
【参考】
- TypeScript 2.0のneverでTagged union typesの絞込を漏れ無くチェックする – Qiita
- Never Type – TypeScript Deep Dive 日本語版
Storeに型をつける
store自体に型をつけるわけではないですが、各moduleで定義した型をここでまとめます。
一番下の行のActionはreduxで用意されている型です。
Container Componentに型をつける
これで最後です。
container componentというのはReduxと接続しているコンポーネントのことです。
まずはstateやactionをpropsに変換する関数に型を付けます。
Dispatch<T>
はreduxで用意されている型です。
次に、containerのpropsに型を付けます。
上記2つの関数のReturnTypes
を使います。
おわり
お疲れ様でした。
これでプロダクト全体に型が行き渡り、再び息を吹き返しました。
こんな感じで型を当てていくと、楽に、安全に、当てられるのではないでしょうか。