概要
FlowとTypeScriptの実際の違いについて、Todoアプリで試してみることにしました。
Todoアプリのソース自体は、Reduxの公式サイトからExample: Todo Listをそのまま使用しています。
参考になった記事
TypeScriptのサンプルコードはこちらが見やすいです。
- ReactをTypeScriptで書ける環境で、ReduxのTutorialをしてみる
- TodoList簡易版@Typescript+React+Redux
- Redux Example の TODO List を TypeScript で作成
- react-redux-typescript-guide
Flowのサンプルコードはこちらが見やすいです。
Flow vs TypeScript
ここから実際の記述です。(🔰初心者です。予めご了承ください🙇♂)
型定義ファイル
types/index.js → types/index.ts
[Flow] types/index.js
// @flow
import type { Store as ReduxStore, Dispatch as ReduxDispatch } from 'redux'
import type { TodosState, TodosAction } from './todos'
import type {
VisibilityFilterState,
VisibilityFilterAction,
} from './visibilityFilter'
type ReduxInitAction = { type: '@@redux/INIT' }
export type State = TodosState & VisibilityFilterState
export type Action = ReduxInitAction | TodosAction | VisibilityFilterAction
export type Store = ReduxStore<State, Action>
export type Dispatch = ReduxDispatch<Action>
[TS] types/index.ts
import { TodosState, TodosAction } from './todos'
import {
VisibilityFilterState,
VisibilityFilterAction,
} from './visibilityFilter'
export type State = TodosState & VisibilityFilterState
export type Action = TodosAction | VisibilityFilterAction
types/todos.js → types/todos.ts
[Flow] types/todos.js
// @flow
export type Id = number
export type Text = string
export type Todo = {
+id: Id,
+text: Text,
+completed: boolean,
}
export type Todos = Array<Todo>
export type TodosState = {
+todos: Todos,
}
export type AddTodoAction = {
type: 'ADD_TODO',
id: Id,
text: Text,
}
export type ToggleTodoAction = {
type: 'TOGGLE_TODO',
id: Id,
}
export type TodosAction = AddTodoAction | ToggleTodoAction
[TS] types/todos.ts
import { Action } from 'redux'
export type Id = number
export type Text = string
export type Todo = {
id: Id
text: Text
completed: boolean
}
export type Todos = Todo[]
export type TodosState = {
todos: Todos
}
export interface AddTodoAction extends Action {
type: 'ADD_TODO'
id: Id
text: Text
}
export interface ToggleTodoAction extends Action {
type: 'TOGGLE_TODO'
id: Id
}
export type TodosAction = AddTodoAction | ToggleTodoAction
types/visibilityFilter.js → types/visibilityFilter.ts
こちらのファイルはFlowとTypeScriptであまり違いはありません。
// @flow 👈 これがなくなるだけ
export type VisibilityFilter = 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED'
export type VisibilityFilterState = {
visibilityFilter: VisibilityFilter
}
export type VisibilityFilterAction = {
type: 'SET_VISIBILITY_FILTER'
filter: VisibilityFilter
}
Actions
actions/index.js → actions/index.ts
こちらのファイルはFlowとTypeScriptであまり違いはありません。
// @flow 👈 これがなくなるだけ
import { Id, Text, TodosAction } from '../types/todos'
import {
VisibilityFilter,
VisibilityFilterAction,
} from '../types/visibilityFilter'
let nextTodoId = 0
export const addTodo = (text: Text): TodosAction => ({
type: 'ADD_TODO',
id: nextTodoId++,
text,
})
export const toggleTodo = (id: Id): TodosAction => ({
type: 'TOGGLE_TODO',
id,
})
export const setVisibilityFilter = (
filter: VisibilityFilter
): VisibilityFilterAction => ({
type: 'SET_VISIBILITY_FILTER',
filter,
})
Reducers
reducers/index.js → reducers/index.ts
combineReducers
のところで書き方が少し違います。
[Flow] reducers/index.js
// @flow
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
import type { Action } from '../types'
export default combineReducers<Object, Action>({
todos,
visibilityFilter,
})
[TS] reducers/index.ts
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
export default combineReducers({
todos,
visibilityFilter,
})
reducers/todos.js → reducers/todos.ts
こちらのファイルはFlowとTypeScriptであまり違いはありません。
[TS] reducers/todos.ts
import { Todo, Todos } from '../types/todos'
import { Action } from '../types'
const todos = (state: Todos = [], action: Action): Todos => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false,
},
]
case 'TOGGLE_TODO':
return state.map(
(todo: Todo): Todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
)
default:
return state
}
}
export default todos
reducers/visibilityFilter.js → reducers/visibilityFilter.ts
こちらのファイルはFlowとTypeScriptであまり違いはありません。
[TS] reducers/visibilityFilter.ts
import { VisibilityFilter } from '../types/visibilityFilter'
import { Action } from '../types'
const visibilityFilter = (
state: VisibilityFilter = 'SHOW_ALL',
action: Action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
export default visibilityFilter
containers
Reduxのconnect
やmapDispatchToProps
あたりの実装です。
containers/AddTodo.js → containers/AddTodo.ts
[Flow] containers/AddTodo.js
// @flow
import { connect } from 'react-redux'
import { addTodo } from '../actions'
import TodoForm from '../components/TodoForm'
import type { Dispatch } from '../types'
export type Props = {
dispatch: Dispatch,
}
const mapDispatchToProps = (dispatch: Dispatch) => ({
onSubmit: (text) => dispatch(addTodo(text)),
})
export default connect(
null,
mapDispatchToProps
)(TodoForm)
[TS] containers/AddTodo.ts
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
import TodoForm from '../components/TodoForm'
import { Action } from '../types'
import { Text } from '../types/todos'
export type Props = {
dispatch: Dispatch
}
const mapDispatchToProps = (dispatch: Dispatch<Action>) => ({
onSubmit: (text: Text) => dispatch(addTodo(text)),
})
export default connect(
null,
mapDispatchToProps
)(TodoForm)
containers/FilterLink.js → containers/FilterLink.ts
Dispatch
の参照先が変わるだけです。
[TS] containers/FilterLink.ts
import { Dispatch } from 'redux' // 👈 TS
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
import { State, Action } from '../types'
// 👆 Flowの時
// import type { State, Dispatch } from '../types'
import { VisibilityFilter } from '../types/visibilityFilter'
type OwnProps = {
filter: VisibilityFilter
}
const mapStateToProps = (state: State, ownProps: OwnProps) => ({
active: ownProps.filter === state.visibilityFilter,
})
const mapDispatchToProps = (
dispatch: Dispatch<Action>,
ownProps: OwnProps
) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Link)
containers/VisibleTodoList.js → containers/VisibleTodoList.ts
[TS] containers/VisibleTodoList.ts
import { connect } from 'react-redux'
import { Dispatch } from 'redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { State, Action } from '../types'
import { Todos } from '../types/todos'
import { VisibilityFilter } from '../types/visibilityFilter'
const getVisibleTodos = (todos: Todos, filter: VisibilityFilter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter((t) => t.completed)
case 'SHOW_ACTIVE':
return todos.filter((t) => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
const mapStateToProps = (state: State) => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter),
})
const mapDispatchToProps = (dispatch: Dispatch<Action>) => ({
toggleTodo: (id: number) => dispatch(toggleTodo(id)),
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
components
App.jsx、Footer.jsxは型定義が必要ないため、それ以外の
Link.jsx, Todo.jsx、TodoForm.jsx、TodoList.jsxに型定義していくことにします。
components/Link.jsx → components/Link.tsx
[Flow] components/Link.jsx
// @flow
import React from 'react'
import type { Node } from 'react'
export type Props = {
active: boolean,
children?: Node,
onClick: () => void,
}
const Link = ({ active, children, onClick }: Props) => (
<button
onClick={onClick}
disabled={active}
style={{
marginLeft: '4px',
}}
>
{children}
</button>
)
export default Link
[TS] components/Link.tsx
import * as React from 'react'
export type Props = {
active: boolean
children?: React.ReactNode
onClick: () => void
}
const Link: React.SFC<Props> = ({ active, children, onClick }: Props) => (
<button
onClick={onClick}
disabled={active}
style={{
marginLeft: '4px',
}}
>
{children}
</button>
)
export default Link
components/Todo.jsx → components/Todo.tsx
こちらのファイルはFlowとTypeScriptであまり違いはありません。
[TS] components/Todo.tsx
import * as React from 'react'
export type Props = {
onClick: () => void
completed: boolean
text: string
}
const Todo: React.SFC<Props> = ({ onClick, completed, text }: Props) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none',
}}
>
{text}
</li>
)
export default Todo
components/TodoForm.jsx → components/TodoForm.tsx
イベントハンドラ系がFlowとTypeScriptで違いがあります。
[Flow] components/TodoForm.jsx
// @flow
import React from 'react'
import type { Text } from '../types/todos'
export type Props = {
onSubmit: (text: Text) => void,
}
const TodoForm = ({ onSubmit }: Props) => {
const [todoName, setTodoName] = React.useState<Text>('')
const handleChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
setTodoName(event.target.value)
}
const handleSubmit = (event: SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault()
onSubmit(todoName)
setTodoName('')
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={todoName} onChange={handleChange} />
<button type="submit">Add Todo</button>
</form>
</div>
)
}
export default TodoForm
[TS] components/TodoForm.tsx
import React from 'react'
import { Text } from '../types/todos'
export type Props = {
onSubmit: (text: Text) => void
}
const TodoForm: React.SFC<Props> = ({ onSubmit }: Props) => {
const [todoName, setTodoName] = React.useState<Text>('')
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setTodoName(event.target.value)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
onSubmit(todoName)
setTodoName('')
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={todoName} onChange={handleChange} />
<button type="submit">Add Todo</button>
</form>
</div>
)
}
export default TodoForm
components/TodoList.jsx → components/TodoList.tsx
こちらのファイルもFlowとTypeScriptで違いはさほどありません。
[Flow] components/TodoList.jsx
import React from 'react'
import Todo from './Todo'
import { Todos, Id } from '../types/todos'
export type Props = {
todos: Todos
toggleTodo: (id: Id) => void
}
const TodoList: React.SFC<Props> = ({ todos, toggleTodo }: Props) => (
<ul>
{todos.map((todo) => (
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
))}
</ul>
)
export default TodoList
終わりに
Todoアプリは非同期処理がないので、アプローチとともに、Redux-thunk × TypeScript版もQiitaの記事を書こうと思います!