13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-02

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

こちらをTypeScripで書いて見ました -> Example: Todo List - Redux

コードはこちら


参考


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


store

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

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例

export enum actionTypes {
	ADD_TODO,
	SET_VISIBILITY_FILTER,
	TOGGLE_TODO,
}

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

Screen Shot 2018-12-02 at 16.48.09.png

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

Screen Shot 2018-12-02 at 16.50.10.png


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
  }
})
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などのアクションの引数、戻り値の型定義をする、ということをしようと思って試行錯誤した結果こうなりました。

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

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

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

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

// 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,
  }
})

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

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の型を指定したぐらいです。

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

container

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)

dispachimport { Dispatch } from 'redux'のやつを使うと良いようです。

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

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

component

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

13
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?