JavaScript
TypeScript
React
redux

typescriptでreact/reduxアプリを作るときにどこに何をどのように書けばいい感じになるか(の一例)

概要

typescriptを勉強しはじめてみて、すごく便利なことはわかったが、react/reduxアプリを書くときにどのようにすればtypescriptの恩恵をより良く受けられるのかがわからなかったので、調べてまとめてみた。この記事で書くのはあくまで個人的に気に入った方法であり、微妙なところや他の方法のほうが良いところがかなりあると思うので、ぜひ教えてください。

読んだ資料

typescriptと、typescript/react/reduxの組み合わせ方について勉強した内容をまとめておく。

まず、react云々の前に、typescriptの基礎を理解していなかったので、公式のドキュメントを読んだ。難しそうなところやとりあえず必要なさそうな雰囲気がするところは適当に読み飛ばしつつ通読した(読み飛ばさず全部読むのが良いと思います)。

次に、確認の意味もこめて以下のqiita記事を読んだ。内容が大変わかりやすいのと、日本語なので読みやすい。

最後に、typescriptとredux+reactの組み合わせ方を勉強した。特にreduxのaction creator周りは普通に書くとtypescriptと組み合わせるのが難しいようで、いろんな工夫の仕方があることがわかった。特に参考になったのは以下。

いくつかの記事の方法の良さそうなところを取り上げて、自分なりに納得できる方針を考えてみた。

Todoアプリの例

typescriptでreact/reduxアプリを書く方針を考える/記事にするために、例としてtypescript/react/reduxでTodoアプリを作ってみた。アプリの仕様は 公式ドキュメントのTodoアプリ とだいたい同じです。

githubレポジトリはこちら -> todo-ts
デモ -> github pages

ディレクトリ構成は以下の通り。最近知ったDucks Patternというパターンに従ってmoduleに分けている(と言っても今回はmoduleが1つしかないが)。

src
├── components
│   ├── App.tsx
│   ├── FilterSelect.tsx
│   ├── Form.tsx
│   └── TodoList.tsx
├── index.tsx
└── redux
    ├── modules
    │   └── todos.ts
    ├── store.ts
    └── types.ts

actionについて

人それぞれでもっともやり方が分かれそうなaction周りの定義。今回は、Union型を使って、moduleごとに全てのactionをまとめた型を作ることにした。javascriptで書くと、タイポを防いだり補完を効かせる目的でconst ADD_TODO = 'ADD_TODO'みたいな定義をすると思うが、typescriptだと文字列リテラルが型として働くので以下のように書くだけで快適になる。

export interface Action<T> {
  type: T;
}
export interface PayloadAction<T, P> {
  type: T;
  payload: P;
}

// moduleごとに全てのactionをまとめた型を作る
export type TodosAction =
  | PayloadAction<'ADD_TODO', { title: string }>
  | PayloadAction<'TOGGLE_TODO', { id: string }>
  | PayloadAction<'CHANGE_FILTER', { filter: Filter }>;

// ↑のおかげでaction creatorを書くときにいい感じに補完が効くようになっている
export const addTodo = (title: string): TodosAction => ({
  type: 'ADD_TODO',
  payload: { title },
});
export const toggleTodo = (id: string): TodosAction => ({
  type: 'TOGGLE_TODO',
  payload: { id },
});
export const changeFilter = (filter: Filter): TodosAction => ({
  type: 'CHANGE_FILTER',
  payload: { filter },
});

reducerについて

reducerは特に悩むところがなかった気がする。actionで型をつけたおかげで補完が気持ちよく効くことに感謝してたら書き終わった。

export const TodosReducer: Reducer<TodosState, TodosAction> = (
  state = todosInitialState, action: TodosAction,
): TodosState => {
  switch (action.type) {
  case 'ADD_TODO':
    return {
      ...state,
      todos: [
        ...state.todos,
        { id: uuid(), title: action.payload.title, completed: false },
      ],
    };
  case 'TOGGLE_TODO':
    return {
      ...state,
      todos: state.todos.map((todo) => (todo.id !== action.payload.id) ? todo : {
        ...todo,
        completed: !todo.completed,
      }),
    };
  case 'CHANGE_FILTER':
    return {
      ...state,
      filter: action.payload.filter,
    };
  default:
    return state;
  }
};

componentについて

bindActionCreatorを使うと、DispatchActionをうまく書くのが難しい。今回は、bindActionCreatorを使いつつちゃんと型が働くように、それぞれのcomponentで使うactionをまとめたobjectを作った。

// このcomponentで使う全てのactionをまとめたobjectを作る
const actions = { toggleTodo };

interface StateProps {
  visibleTodos: Todo[];
}
// DispatchPropsはactionsを使ってこのように定義しておくと良い
type DispatchProps = typeof actions;

const TodoListPresentation = (props: StateProps & DispatchProps) => (
  <div>
    <ul>
      {props.visibleTodos.map((todo) => (
        <li
          key={todo.id}
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          onClick={() => props.toggleTodo(todo.id)}
        >
          {todo.title}
        </li>
      ))}
    </ul>
  </div>
);

const mapStateToProps = (state: AppState): StateProps => {
  const { todos, filter } = state.todos;

  return {
    visibleTodos: filterTodos(todos, filter),
  };
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
  return {
    ...bindActionCreators(actions, dispatch),
  };
};

まとめ

typescriptでreact/reduxアプリを書く上で、とりあえず今の自分が納得できる方針を見つけられて良かった。これからtypescriptの理解が進むにつれて、書き方をアップデートしていきたい。より良い方法があると思うので、ぜひ教えてください。