TypeScript、Redux初心者です。ググりながら書きました、間違ってましたら指摘していただきたく。よろしくお願いいたします。
こちらをTypeScripで書いて見ました -> Example: Todo List - Redux
【コードはこちら】
参考
- piotrwitek/react-redux-typescript-guide: The complete guide to static typing in "React & Redux" apps using TypeScript
- Armour/express-webpack-react-redux-typescript-boilerplate: A full-stack boilerplate that using express with webpack, react and typescirpt!
- training4developers/using-react-with-redux: Using React with Redux
- TypeScript 2.4+におけるRedux Action - Qiita
- メンテナブルな React Component を目指すための小技集 - Qiita
- TodoList簡易版@Typescript+React+Redux - Qiita
- React & Redux & Typescript デビューメモ _φ(・・
以下、自分が工夫した点などを部分的に紹介していきます。
store
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
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,
}
動作の上では問題ないが、このようにデバッグツールだとどのアクションが実行されたのかわかりにくい
enumでもアクション名の文字列を代入しておくとどのアクションが実行されたのかわかりやすい!
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
}
})
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
などのアクションの引数、戻り値の型定義をする、ということをしようと思って試行錯誤した結果こうなりました。
↓自分が工夫したのはこのようなことです。
// 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の型を指定したぐらいです。
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がエラー出してくれるためやりやすいなと思いました。
container
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
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