ReduxチュートリアルをTypeScriptでやってみた

Reactのチュートリアルが終わったので、今度はReduxのチュートリアルBasics · Redux)をTypeScriptでやってみました。

これは非常に大変でした。

はじめに

ReactのチュートリアルTutorial: Intro To React - React)は、TypeScriptでまあまあ順調にやれたのですが、Reduxの方は激ムズでした。

これはちょっと初心者が手を出すべきものではないですね。

最初はTypeScriptなしでやり、ある程度慣れてきたらTypeScriptで書き直ししてみるのがいいと思います。

実はまだ解決できていない部分があるのですが、一応、自分がやったことを記録しておこうと思います。

基本的な修正

TypeScriptでやる場合、型の指定を行うことがメインになります。

ただし、Reduxの機能を使う場合、簡単に型を推測できない場合があります。

その場合、以下のようなサンプルコードが参考になりました:

また、Presentational Component がファンクションになっているので、React.Component から継承したクラスに直しました。

Actionのインターフェース定義

Actionについては、Reduxでベースのインターフェースが提供されているので、一応それを継承するようにしました。

import { Action } from 'redux';

export interface IAddTodoAction extends Action {
  type: string;
  id: number;
  text: string;
}

また、Reducerでは任意のActionを受け取って処理を分岐することになるので、複数の異なるActionを1つの型に統合する必要があります:

// actions/index.ts
export type TodoAction = IAddTodoAction | ISetVisibilityFilterAction | IToggleTodoAction;

// reducers/todos.ts
const todos = (state: State = [], action: TodoAction) => {...}

追記(2017-07-18):

統合した型を指定しなくても、Action を指定すれば大丈夫でした。

追記終了

また、中の処理でActionごとに処理を分岐する必要がありますが、Actionのインターフェースが異なる場合はキャストする必要があります:

const todos = (state: State = [], action: TodoAction) => {
  switch (action.type) {
    case 'ADD_TODO':
      const a1 = <IAddTodoAction> action;
      return [
        ...state,
        {
          id: a1.id,
          text: a1.text,
          completed: false
        }
      ];
...

dispatchの型

Containerの関数 mapDispatchToProps の引数に dispatch があるのですが、これはReduxの Dispatch になります。

ただし、ジェネリックなので、Actionのインターフェースを指定する必要があります。

import { Dispatch } from 'redux';

const mapDispatchToProps = (dispatch: Dispatch<IToggleTodoAction>) => {...}

FilterLink containerについて

FilterLinkについては、結局、エラーを解消できませんでした。

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link);  // ここでエラー

エラーメッセージ:

[ts]
Argument of type 'typeof Link' is not assignable to parameter of type 'Component<IProps & { active: boolean; } & { onClick: () => void; }>'.
  Type 'typeof Link' is not assignable to type 'StatelessComponent<IProps & { active: boolean; } & { onClick: () => void; }>'.
    Type 'typeof Link' provides no match for the signature '(props: IProps & { active: boolean; } & { onClick: () => void; } & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

propsの食い違いのようですが、よくわかりませんでした。

他のContainerでは同様の実装で問題なかったので、FilterLinkの構造が影響しているのかもしれません。

後でわかったら更新します。

追記(2017-07-26)

ようやくわかりました。

Link の props のインターフェースに、FilterLink の ownProps のインターフェースの要素を含むようにしたら解消しました。

component は container の props を継承しないといけない、みたいな思想ですかね。ちょっと違和感がありますが。

追記終了

追記(2017-07-29)

ownProps は Component に指定された props ですね。

誤解していました。

追記終了

AddTodo container について

この部分に関しては、container内にJSXが書かれていて、ContainerとPresentational Componentがミックスされたような形になっています。

恐らく、そういったサンプルとして入れたのだと思いますが、React.Componentを継承したクラスに直すのが難しそうだったので、ファンクションのままにしました。

その場合に、パラメーターの { dispatch } が問題になってきます。

これは、「分割代入」(Destructuring assignment) という記法で、パラメーターがオブジェクトで渡ってきて、その dispatch という要素が dispatch という変数に代入されることになります。

この型を定義する場合、{ dispatch }: IParam のような形で指定することになりますが、dispatch が関数なので IParam の指定が複雑になります。

結局、ここは { dispatch }: any で逃げてしまいました。(ワーニングが出ます。)

また、input変数の型もよくわからなかったので、let input: any; で逃げました。

あとがき

Reactのチュートリアルの時は最終的にスッキリしたのですが、Reduxの方はすべてを解決することができず、TypeScriptでやっていく上で少し不安が残る結果となってしまいました。

今後、さらにReduxでの実装を経験して、いずれ解決していきたいと思います。