ReduxチュートリアルのTODOリストをTypeScriptに書き換えて勉強

TypeScript、Redux初心者です。ググりながら書きました、間違ってましたら指摘していただきたく。よろしくお願いいたします。

こちらをTypeScripで書いて見ました -> [Example: Todo List - Redux](https://redux.js.org/basics/exampletodolist)

【[コードはこちら](https://github.com/okumurakengo/00002)】

---

参考

- [piotrwitek/react-redux-typescript-guide: The complete guide to static typing in "React & Redux" apps using TypeScript](https://github.com/piotrwitek/react-redux-typescript-guide)
- [Armour/express-webpack-react-redux-typescript-boilerplate: A full-stack boilerplate that using express with webpack, react and typescirpt!](https://github.com/Armour/express-webpack-react-redux-typescript-boilerplate)
- [training4developers/using-react-with-redux: Using React with Redux](https://github.com/training4developers/using-react-with-redux)
- [TypeScript 2.4+におけるRedux Action - Qiita](https://qiita.com/akisx/items/40c6785ae696144407b0)
- [メンテナブルな React Component を目指すための小技集 - Qiita](https://qiita.com/terrierscript/items/372727448b58d4738018)
- [TodoList簡易版@Typescript+React+Redux - Qiita](https://qiita.com/knknkn1162/items/94543bf8c0701f25ac77)
- [React & Redux & Typescript デビューメモ _φ(･･](https://qiita.com/hitomi_/items/ad27e75db37b38f86db7)

---

以下、自分が工夫した点などを部分的に紹介していきます。

---

# store

```ts:src/store.ts
import { createStore, Store } from 'redux'

import rootReducer from './reducers'
import { VisibilityFiltersEnum } from './actions/types'

export type todosState = Array<{
    id: number;
    text: string;
    completed: boolean;
}>

export interface IState {
    todos: todosState;
    visibilityFilter: VisibilityFiltersEnum;
    nextTodoId: number;
}

export default createStore(rootReducer) as Store<IState>;
```

TODOリストの配列の指定で `Array<{...}>` という書き方を探すのに時間がかかった、、

# action

```ts:src/actions/action-types.ts
export enum actionTypes {
	ADD_TODO = "ADD_TODO",
	SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER",
	TOGGLE_TODO = "TOGGLE_TODO",
}
```

enumで列挙。自分はenumだから同じ名前の文字列を代入しなくてもいいかなと思いましたが、デバッグツールで見ずらいのでやめました。

NG例

```ts
export enum actionTypes {
	ADD_TODO,
	SET_VISIBILITY_FILTER,
	TOGGLE_TODO,
}
```

動作の上では問題ないが、このようにデバッグツールだとどのアクションが実行されたのかわかりにくい

![Screen Shot 2018-12-02 at 16.48.09.png](https://qiita-image-store.s3.amazonaws.com/0/142910/76eecd5e-088f-d415-f188-3ac631647cdc.png)

enumでもアクション名の文字列を代入しておくとどのアクションが実行されたのかわかりやすい!

![Screen Shot 2018-12-02 at 16.50.10.png](https://qiita-image-store.s3.amazonaws.com/0/142910/b1651ce2-8513-adf8-5768-3ff9c8bd64c8.png)

---

```ts:src/actions/index.ts
import { actionTypes } from './action-types'
import {
  IAddTodoAction,
  ISetVisibilityFilterAction,
  IToggleTodoAction,
} from './types'

export const addTodo: IAddTodoAction = (id, text) => ({
  type: actionTypes.ADD_TODO,
  payload: {
    id,
    text,
  }
})

export const setVisibilityFilter: ISetVisibilityFilterAction = filter => ({
  type: actionTypes.SET_VISIBILITY_FILTER,
  payload: {
    filter,
  }
})

export const toggleTodo: IToggleTodoAction = id => ({
  type: actionTypes.TOGGLE_TODO,
  payload: {
    id
  }
})
```

```ts:src/actions/types.ts
import { Action } from 'redux'

import { actionTypes } from './action-types'

interface IAction<T, P> extends Action<T> {
    type: T;
    payload: P;
}

export interface IAddTodo { id: number, text: string }
export interface ISetVisibilityFilter { filter: VisibilityFiltersEnum }
export interface IToggleTodo { id: number }

export interface IAddTodoAction {
    (id: number, text: string): IAction<actionTypes.ADD_TODO, IAddTodo>
}

export interface ISetVisibilityFilterAction {
    (filter: VisibilityFiltersEnum): IAction<actionTypes.SET_VISIBILITY_FILTER, ISetVisibilityFilter>
}

export interface IToggleTodoAction {
    (id: number): IAction<actionTypes.TOGGLE_TODO, IToggleTodo>
}

export type IActions = IAction<actionTypes.ADD_TODO, IAddTodo>
                     | IAction<actionTypes.SET_VISIBILITY_FILTER, ISetVisibilityFilter>
                     | IAction<actionTypes.TOGGLE_TODO, IToggleTodo>

export enum VisibilityFiltersEnum {
    SHOW_ALL,
    SHOW_COMPLETED,
    SHOW_ACTIVE,
}
```

定義ファイルにある `interface Action<T = any>`↓を継承しつつ、 `addTodo`などのアクションの引数、戻り値の型定義をする、ということをしようと思って試行錯誤した結果こうなりました。

↓自分が工夫したのはこのようなことです。

```ts:node_modules/redux/index.d.ts
// reduxの定義ファイル

export interface Action<T = any> {
  type: T
}
```

reduxの定義ファイルのActionを継承したinterfaceをひとつ作り、それを元に型定義する。

```ts
// src/actions/types.ts

interface IAction<T, P> extends Action<T> {
    type: T;
    payload: P; // <- dispachの引数がここに来るようにする
}

// 「(引数1, 引数2, ...) => reduxの定義ファイルのActionを継承した戻り値」 となるように無名関数の型を定義
export interface IAddTodoAction {
    (id: number, text: string): IAction<actionTypes.ADD_TODO, IAddTodo>
}

// ...

// src/actions/index.ts

// ↑の定義のおかげで引数、戻り値の型が不正だとエラーになってくれる
export const addTodo: IAddTodoAction = (id, text) => ({
  type: actionTypes.ADD_TODO,
  payload: {
    id,
    text,
  }
})
```

---

↓もう少しシンプルに書くとするとこうだと思います。これでも動作上は問題ないですが、引数の数が増えていくと見通しが悪くなりそうかな？と思ったため、↑のような書き方にしてみました。

```ts
import { Action } from 'redux'
import { actionTypes } from './action-types'

interface TestAddTodo extends Action<actionTypes.ADD_TODO> {
  type: actionTypes.ADD_TODO,
  payload: {
    id: number, 
    text: string
  }
}

export const addTodo = (id: number, text: string): TestAddTodo => ({
  type: actionTypes.ADD_TODO,
  payload: {
    id,
    text,
  }
})
```

# reducer

typescriptにする上でロジックはあまり変えていません。state、actionの型を指定したぐらいです。

```ts:src/reducers/todos.ts
import { Reducer } from 'redux'

import { todosState } from '../store'
import { actionTypes } from '../actions/action-types'
import { IActions } from '../actions/types'

export default ((state: todosState = [], action: IActions) => {
  switch (action.type) {
    case actionTypes.ADD_TODO:
      return [
        ...state,
        {
          id: action.payload.id,
          text: action.payload.text,
          completed: false
        }
      ]
    case actionTypes.TOGGLE_TODO:
      return state.map(todo =>
        (todo.id === action.payload.id)
          ? {...todo, completed: !todo.completed}
          : todo
      )
    default:
      return state
  }
}) as Reducer<todosState>
```

型を指定したので、不正な値をVSCodeがエラー出してくれるためやりやすいなと思いました。

![2yPsqkY2ni.gif](https://qiita-image-store.s3.amazonaws.com/0/142910/08300528-646c-6fb4-1dac-55fbbf082c3e.gif)

# container

```ts:src/containers/AddTodo.ts
import { Dispatch } from 'redux'
import { connect } from 'react-redux'

import { IState } from '../store'
import { addTodo } from '../actions'
import Addtodo from '../components/Addtodo'
import { IActions } from '../actions/types'

export interface IHandleSubmitArgs {
    e: React.FormEvent<HTMLFormElement>,
    nextTodoId: number,
    input: HTMLInputElement,
}

interface IMapDispatchToProps {
    (dispatch: Dispatch<IActions>): {
        handleSubmit: (handleSubmitArgs: IHandleSubmitArgs) => void
    }
}

const mapDispatchToProps: IMapDispatchToProps = dispatch => ({
    handleSubmit: ({e, nextTodoId, input}) => {
        e.preventDefault()
        if (!input.value.trim()) {
          return
        }
        dispatch(addTodo(nextTodoId, input.value))
        input.value = ''
    }
})

export default connect(
    ({ nextTodoId }: IState): { nextTodoId: number } => ({ nextTodoId }),
    mapDispatchToProps
)(Addtodo)
```

`dispach`は`import { Dispatch } from 'redux'`のやつを使うと良いようです。

引数のstateは自分で作成した`IState`から型チェックするようにした。

componentでのサブミットイベントをひろう時は`React.FormEvent<HTMLFormElement>`を指定すると良いよいだ(間違ってたらごめんなさい)

# component

```tsx:src/components/TodoList.tsx
import * as React from 'react'

import { todosState } from '../store'
import Todo from './Todo'

interface IProps {
  todos: todosState;
  toggleTodo: (id: number) => void
}

export default (
  ({ todos, toggleTodo }) => (
    <ul>
      {todos.map(todo =>
        <Todo
          key={todo.id}
          {...todo}
          onClick={() => toggleTodo(todo.id)}
        />
      )}
    </ul>
  )
) as React.SFC<IProps>
```

componentもtypescriptで書き換えるのに変更点はそんなにありませんでした。

ファンクションのコンポーネントなら、`React.SFC`を指定しておけば良さそうです。

---

最後まで読んでいただいてありがとうございました。m(_ _)m