typescript-fsaに頼らないReact × Redux

甲斐田亮一氏:「typescript-fsaに頼らないReact × Redux」というタイトルで始めさせていただきます。僕は日本事務器という会社でフロントエンドエンジニア兼デザイナーをしている甲斐田といいます。

ふだんの業務で、React、TypeScriptでアプリケーションを書いています。

今回話すことですが、最近のTypeScript 2.8以降の機能を使って、React × Reduxの型付けを行っていきます。なるべくReact × Reduxのテンプレートのような書き方を崩さないように、型を組んでいきたいなと思います。

逆に、Flowやtypesctipt-fsaとの書き方や機能面での比較と、TypeScriptの詳しい型システムの解説は、今回行いません。なので、「最近のTypeScriptだとこういうふうに書けるんだよ」くらいの感じに思ってください。

Stateful Component

最初にStateful Componentについて。

普通のクラスコンポーネントですよね。型はinterfaceかtype aliasを使ってPropsとStateをそれぞれ定義します。TypeScript3.0からdefaultPropsがサポートされていて、defaultPropsに定義したPropsであれば、宣言時に省略してもnullアサーションがされなくなっています。

型注釈をする場合は別途定義する必要があります。方法としてはこんな感じです。

まずtype aliasでPropsとStateを定義して、React.Componentのあとに型引数としてそれぞれ渡してあげます。InitialStateは別途宣言して、それをそのまま型宣言にも利用します。Readonlyを付けて、setState以外からのアクセスを禁止します。

これを見ていただいたらわかるんですが、リードオンリーって2つ出てきてますよね。これらはそれぞれ別のことをやっていて、上のReadonlyではstate.count=1という代入を、下のreadonlyではstate={count:1}という代入をそれぞれ禁止しています。

Functional Component

続いて、Stateless Functional Component。

React.SFCというもので型を付けていきます。defaultPropsを付ける際は、関数のデフォルト引数を利用して付けていきます。

先月のアップデートで追加されたReact.memoはまだ未対応です(注:発表当時。その後対応された。また、Stateless Functional Componentという名称はなくなり、Function Componentsという名称にまとめられた)。

プルリクは作成されていて、このプルリクで、今あれこれいろいろ議論されています。内容をざっと見たんですが、エキゾチックコンポーネントか、SpecialSFCというのが追加されていたので、このどちらかかなといった印象ですね(注:その後、NamedExoticComponentに決定)。

コードはこんな感じで、先ほどとあまり変わりはないんですが。

Propsを受け取る際に分割して、もし値がなければそれぞれ、これらの値を代入するよということをやっています。

ここからReduxの内容に入ってきます。まずはContainerです。ここでは2つのものに型を付けています。

まず1つ目がmapStateToPropsで、こちらは自分でアプリケーション内で使う必要な型を作成して、それを与えていきます。それで、mapDispatchToPropsは、ReduxにあるDispatchという型を使います。

アプリケーションで下側は定義されていて、ページが3つあります。それぞれcalcPage、hogePage、fooPageがあって、それをインポートしてmapStateToPropsのあとにStateを渡してあげる。mapDispatchToPropsのあとは、Dispatchを渡してあげます。このDispatchは、anyというタイププロパティを持つオブジェクトであれば、なんでも許容されるようになっています。

Action

続いてAction。ここからがメインの話です。

typescript-fsaなりredux-actionsは専用の構文だったりaction creatorを作成していますが、素のTypeScriptだとActionのタイププロパティに「as typeof xxxx」で型を付けていきます。

こうすることで、actiontypeをただのstringではなくて、文字列リテラル型の型を付与することができます。コードはこんな感じになっていて、もとのテンプレートの書き方をほぼほぼ崩さずに書けているんじゃないのかなと思います。

Reducer

そしてReducerで、先ほど作成したActionの型を紐づけていきます。

ここからがっつりTypeScriptの機能を使っていて、まずConditional typesのReturnTypeというものを使って、そのReducer内で受け取るActionを定義します。そして、Tagged union typesというもので各Actionが持つプロパティを絞り込みます。最後にnever型を使って、そのReducer内で受け取るActionの定義漏れだったり、タイポがないかをチェックします。

だいぶ小さくてすいません。そもそもこれは何してるかなんですけど、このReturnTypeは関数を受け取るようになっていて、その関数の戻り値の型を抽出するっていうことをやっています。

そして、それをunion typesで結合しています。

フォーカスしてみるとこのように、3つのActionが受け取ることをTypeScript側が認識することができています。

INCREMENT、DECREMENTはtypeだけを持っていて、CALCだったらpayloadを持っているとちゃんと認識しています。payloadは中身がnum1、num2を持っていて、それは型がnumberだということもちゃんと認識してくれています。

これをswitch文で、tagged union typesでActionの絞り込みを行っていきます。

そして、TypeScript側で「このActionだったらこの型を持っている」「payloadを持っている」と認識しているので、CALC内でpayloadに対して直接アクセスすることができています。

最後にnever型。

default部で定義して、もしこちらが予期していないActionが来た場合は、このnever型のアンダーバーの中にActionが流れ込みます。例えばこのDECREMENT定数をコメントアウトすると、例えばこのDECREMENTケースをコメントアウトすると、このReducerはINCREMENT、DECREMENT、CALCという3つのActionを期待しているのに、DECREMENTの受け取り口がなくなるので、DECREMENTがdefault区に流れ込みます。

そこでこのアンダーバーに、never型にアサインされてしまうので、TypeScriptがエラーを発動してしまう。なのでここで、その次が実行できなくなってしまいます。こうすることでReducerを型安全に、かつ型の恩恵を受け取りつつ書くことができます。

ただ、気を付けることがあります。

受け取るActionが1つのときは、このnever型が使えないんですよね。なのでnever型を消すか、普通にif文で書けば書けるようになります。JavaScriptだと、switch文に入る前にActionを分割代入で先に取ってから、switch文でpayloadのアクセスができるようになっていたんですが、それは今回の書き方ではできません。

Middleware

最後、Middleware。

今回Middlewareは、素のMiddlewareで作りました。store、next、actionにそれぞれ型を付けます。actionの絞り込みには先ほどのConditional typesを利用できます。今回は書き方だけ紹介します。実際にアプリケーションで使うときは素のMiddlewareではなくて、sagaなりthunk、observablesを使うのがいいです。

コードとしてはこんなふうになっています。

storeにMiddlewareAPIというのを渡してあげて、Dispatchとそのアプリケーションで使うstoreを渡してあげます。ReduxはMiddlewareという型定義をちゃんと定義しているんですが、その場合はactionの型推論がanyになっているので。actionの型推論が一切なくなります。なので僕は書くとしたらこういうふうに書きます。

まとめです。

2.8以降の機能であれば素のTypeScriptでもいい感じに型を付けていけてるんじゃないのかなと思います。僕はもともとFlowを使っていて、TypeScriptって敬遠してたんですね。というのが、サードパーティを使わないといけないし、書き方が大きく崩れるなと思っていて、やってなかったんですが。

最近のTypeScriptだったらそのテンプレートを崩さずに、さらに型の恩恵にあやかりつつ書くことができるので、いいんじゃないのかなと思います。なのでFlowとかで迷っている人がいれば、TypeScriptを検討してみてもいいんじゃないのかなと思います。以上です、ご清聴ありがとうございました。

(会場拍手)