Help us understand the problem. What is going on with this article?

Flow と TypeScript のTodoチュートリアルコード集

More than 1 year has passed since last update.

概要

FlowとTypeScriptの実際の違いについて、Todoアプリで試してみることにしました。
Todoアプリのソース自体は、Reduxの公式サイトからExample: Todo Listをそのまま使用しています。

参考になった記事

TypeScriptのサンプルコードはこちらが見やすいです。

Flowのサンプルコードはこちらが見やすいです。

Flow vs TypeScript

ここから実際の記述です。(🔰初心者です。予めご了承ください🙇‍♂)

型定義ファイル

types/index.js → types/index.ts

1.png

[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

2.png

[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のところで書き方が少し違います。

3.png

[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であまり違いはありません。

4.png

[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であまり違いはありません。

5.png

[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のconnectmapDispatchToPropsあたりの実装です。

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の参照先が変わるだけです。

6.png

[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

こちらもDispatchの参照先が変わるだけです。
7.png

[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

8.png

[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であまり違いはありません。

9.png

[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で違いがあります。
10.png

[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で違いはさほどありません。
11.png

[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の記事を書こうと思います!

Hitomi_Nagano
フロントエンドエンジニア
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away