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

Redux ExampleのTodo ListをはじめからていねいにVuex+Typescriptで(1)

概要

Redux ExampleのTodo ListをはじめからていねいにをVue.js*Typescriptを使って行ったメモ。

環境

  • Windows 10
  • Vagrant 2.2.5
  • virtualbox 6.0.10
  • Ubuntu 18.04 LTS (Bionic Beaver)
  • Docker version 18.09.5, build e8ff056
  • docker-compose version 1.24.0, build 0aa59064

仮想環境のIPは192.168.50.10に指定。

ブラウザはchromeで確認。

ディレクトリ構成(Hello world時)

  • vue-cliで生成されるソースコードを参考としている。
.
├── bin # docker-composeの操作をシェル化
│   ├── bash.sh # コンテナ内にbashでログイン
│   ├── build.sh # dist内にjsファイルをビルド
│   ├── container_build.sh # 作業用コンテナ作成
│   ├── fix.sh # lintによるソースコードの自動修正
│   └── up.sh # 開発サーバの起動
├── dist # ビルドされたファイルの格納先
├── docker
│   ├── config # コンテナ内にコピーされる設定ファイル
│   │   ├── .browserslistrc    # 対応ブラウザ設定
│   │   ├── .eslintrcjs        # lint設定
│   │   ├── cypress.json       # テストランナー設定
│   │   ├── jest.config.js     # テストツール設定
│   │   ├── package.json       # npm設定ファイル
│   │   ├── postcss.config.js  # postcss設定ファイル
│   │   ├── tsconfig.json      # typescript設定ファイル
│   │   └── vue.config.js      # webpack設定ファイル
│   ├── docker-compose.yml      # コンテナ起動時設定ファイル
│   └── vue
│       └── Dockerfile          # イメージ作成用のコマンド記述ファイル
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── assets
│   │   └── logo.png
│   ├── components # Vueコンポーネント
│   │   └── HelloWorld.vue
│   ├── App.vue # Appコンポーネント
│   ├── main.ts # エントリーポイント
│   ├── shims-tsx.d.ts # 型定義ファイル
│   ├── shims-vue.d.ts # 型定義ファイル
│   └── types.d.ts # 型定義ファイル
├── tests
│   ├── e2e
│   │   ├── plugins
│   │   │   └── index.js
│   │   ├── specs
│   │   │   └── test.js
│   │   └── support
│   │       ├── commands.js
│   │       └── index.js
│   └── unit
│       └── example.spec.ts
└── .gitignore

ビルドツール

docker/vue/Dockerfile
FROM node:12.9.1

# コンテナ上の作業ディレクトリ作成
WORKDIR /app
#COPY ./* /app/
#RUN yarn install
RUN yarn add --dev \
  @babel/core \
  @babel/preset-env \
  babel-loader \
  css-loader \
  vue-loader \
  vue-style-loader \
  vue-template-compiler \
  webpack \
  webpack-cli \
  webpack-dev-server
RUN yarn add vue vue-custom-element
RUN npm install -g @vue/cli
RUN yarn add --dev typescript

RUN yarn add --dev ts-loader
RUN yarn add vue-class-component 
RUN yarn add vue-property-decorator
RUN yarn add --dev html-webpack-plugin
RUN yarn add --dev @types/jest
RUN yarn add --dev @vue/cli-plugin-e2e-cypress
RUN yarn add --dev @vue/cli-plugin-eslint
RUN yarn add --dev @vue/cli-plugin-typescript
RUN yarn add --dev @vue/cli-plugin-unit-jest
RUN yarn add --dev @vue/cli-service
RUN yarn add --dev @vue/eslint-config-airbnb
RUN yarn add --dev @vue/eslint-config-typescript
RUN yarn add --dev @vue/test-utils
RUN yarn add --dev babel-eslint
RUN yarn add --dev eslint
RUN yarn add --dev eslint-plugin-vue
RUN yarn add --dev ts-jest
RUN yarn add --dev typescript
RUN yarn add --dev vue-template-compiler

RUN yarn add --dev @types/firebase
RUN yarn add --dev vuex
docker/docker-compose.yml
version: '3'
services:
  web_components_vue:
    build: ./vue
    ports:
      - 8080:8080
    volumes:
      - ../src:/app/src
      - ../public:/app/public
      - ../tests:/app/tests
      - ./config/.browserslistrc:/app/.browserslistrc
      - ./config/.eslintrc.js:/app/.eslintrc.js
      - ./config/cypress.json:/app/cypress.json
      - ./config/jest.config.js:/app/jest.config.js
      - ./config/package.json:/app/package.json
      - ./config/postcss.config.js:/app/postcss.config.js
      - ./config/tsconfig.json:/app/tsconfig.json
      - ./config/vue.config.js:/app/vue.config.js
      - ../dist:/dist
    command: [yarn, serve ]
docker/config/package.json
{
  "name": "vue-typescript",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build && cp -r /app/dist/* /dist/",
    "lint": "vue-cli-service lint",
    "test:e2e": "vue-cli-service test:e2e",
    "test:unit": "vue-cli-service test:unit"
  },
  "dependencies": {
    "vue": "^2.6.10",
    "vuex": "^3.1.1",
    "vue-class-component": "^7.1.0",
    "vue-property-decorator": "^8.2.2"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "@types/jest": "^24.0.18",
    "@vue/cli-plugin-e2e-cypress": "^3.11.0",
    "@vue/cli-plugin-eslint": "^3.11.0",
    "@vue/cli-plugin-typescript": "^3.11.0",
    "@vue/cli-plugin-unit-jest": "^3.11.0",
    "@vue/cli-service": "^3.11.0",
    "@vue/eslint-config-airbnb": "^4.0.1",
    "@vue/eslint-config-typescript": "^4.0.0",
    "@vue/test-utils": "^1.0.0-beta.29",
    "babel-eslint": "^10.0.3",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.2.0",
    "eslint": "^6.3.0",
    "eslint-plugin-vue": "^5.2.3",
    "html-webpack-plugin": "^3.2.0",
    "ts-jest": "^24.0.2",
    "ts-loader": "^6.0.4",
    "typescript": "^3.6.2",
    "vue-loader": "^15.7.1",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.39.3",
    "webpack-cli": "^3.3.7",
    "webpack-dev-server": "^3.8.0"
  }
}
docker/config/vue.config.js
module.exports = {
  configureWebpack: {
    // ビルド高速化のために外部からvue.jsを読み込む
    externals: {
      vue: 'Vue',
      vuex: 'Vuex',
    },
  },
  devServer: {
    // sock.js用に仮想環境のIPとポートを指定
    public: '192.168.50.10:8080',
    // vagrantの仕様でポーリングしないとファイルの変更を感知できない
    watchOptions: {
      poll: true,
    },
    disableHostCheck: true,
    hotOnly: true,
    clientLogLevel: 'warning',
    inline: true,
  },
};
.browserslistrc
last 1 chrome version
.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: [
    'plugin:vue/essential',
    '@vue/airbnb',
    '@vue/typescript',
  ],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'max-len': ['error', { code: 140 }],
    'import/extensions': 'off',
  },
  parserOptions: {
    parser: '@typescript-eslint/parser',
  },
  overrides: [
    {
      files: [
        '**/__tests__/*.{j,t}s?(x)',
      ],
      env: {
        jest: true,
      },
    },
  ],
};
docker/config/postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: {},
  },
};

docker/config/tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "types": [
      "webpack-env",
      "jest"
    ],
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

開発サーバ用html

開発サーバにはwebpack-dev-serverを利用する。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>TodoList</title>
</head>

<body>
      <!-- vueのコンポーネントを#root以下に作成する設定にしている -->
      <div id="app"></div>
      <!-- vueを外部から読み込む -->
      <script src="https://cdn.jsdelivr.net/npm/vue"></script>
      <script src="https://unpkg.com/vuex@3.1.1/dist/vuex.js"></script>
</body>

</html>

shell

docker-composeのコマンドを毎回タイプするのが面倒なのでシェルにしている。

bin/up.sh
#!/bin/bash

# このシェルスクリプトのディレクトリの絶対パスを取得。
bin_dir=$(cd $(dirname $0) && pwd)
parent_dir=$(cd $bin_dir/.. && pwd)
docker_dir=$(cd $parent_dir/docker && pwd)
composeFile=${1:-"docker-compose.yml"}

# docker-composeの起動
cd $docker_dir && docker-compose -f $composeFile up
bin/fix.sh
#!/bin/bash

bin_dir=$(cd $(dirname $0) && pwd)
parent_dir=$(cd $bin_dir/.. && pwd)
docker_dir=$(cd $parent_dir/docker && pwd)
container_name=${1:-web_components_vue}

# $container_nameの有無をgrepで調べる
docker ps | grep $container_name

# grepの戻り値$?の評価。 grep戻り値 0:一致した 1:一致しなかった
if [ $? -eq 0 ]; then
  # 一致したときの処理
  cd $docker_dir && docker-compose exec --env NODE_ENV=development $container_name yarn lint --fix
else
  # 一致しなかった時の処理
  # コンテナを立ち上げて接続
  cd $docker_dir && docker-compose run -e NODE_ENV=development $container_name yarn lint --fix
fi
bin/build.sh
#!/bin/bash

bin_dir=$(cd $(dirname $0) && pwd)
parent_dir=$(cd $bin_dir/.. && pwd)
docker_dir=$(cd $parent_dir/docker && pwd)
container_name=${1:-web_components_vue}

# 出力ディレクトリのクリーン
rm -rf $parent_dir/dist/js 

docker ps | grep $container_name
if [ $? -eq 0 ]; then
  cd $docker_dir && docker-compose exec --env NODE_ENV=production $container_name yarn build
else
  cd $docker_dir && docker-compose run -e NODE_ENV=production $container_name yarn build
fi

1. Hello world

src/main.ts
import Vue from 'vue';
import App from './App.vue';

new Vue({
  render: (h: (app: any) => Vue.VNode) => h(App),
}).$mount('#app');
src/App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import HelloWorld from "./components/HelloWorld.vue";

@Component({
  components: {
    HelloWorld
  }
})
export default class App extends Vue {}
</script>

<style>
#app {
  text-align: center;
  margin-top: 60px;
}
</style>
src/components/HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  margin: 40px 0 0;
}
</style>

実行

開発用サーバを起動。

./bin/up.sh

ブラウザでアクセスして確認。

http://192.168.50.10:8080/

この時点のソース

2. actionでcommitを呼び出してmutationでstateを更新する

Reduxの「2. actionCreatorで発行したactionをreducerに渡してstoreのstateを更新する」に該当する部分。

Store

Reduxとほぼ同じ概念。
アプリケーションで単一のもので、stateを保持する。

  • ストアの状態を直接変更することはできない。明示的にミューテーションをコミットすることによってのみ、ストアの状態を変更する。
src/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';

// ルートコンポーネントに store オプションを指定することですべての子コンポーネントにストアを注入。
Vue.use(Vuex);

// 各プロパティの詳細は後述

export default new Vuex.Store({
  state,
  mutations,
  getters,
  actions,
});

State

ReduxでいうところのStore。
アプリケーションで単一のもので、state(状態)を保持する。

src/store/index.ts
export type TodoItem = {
  id: number;
  text: string;
}

export interface State {
  todos: TodoItem[];
}

// 状態管理用state
export const state: State = ({ todos: [] } as State);

Getter

ストアの状態を算出したいときに使える。
例えば項目のリストをフィルタリングしたりカウントしたりできる。

src/store/index.ts
// getter関数の定義でプログラマが自由に決めることができるのは「関数名」と「戻り型」のみ。
interface IGetters {
  // 関数名:戻り型
  todos: TodoItem[];
  todosCount: number;
}

// getter関数の引数は固定のため、インデックスシグネチャを利用して全てのgetter関数にState型とgetter関数の型参照を定義
type Getters<S, G, RS = {}, RG = {}> = {
  // [K in keyof G]: 定義されている関数名を取得
  // G[K] : 取得した戻り型を付与
  // RS,RG : 第三引数、第四引数については保留
  [K in keyof G]: (state: S, getters: G, rootState: RS, rootGetters: RG) => G[K]
}

// 値の取得
export const getters: Getters<State, IGetters> = {
  todos: () => state.todos,
  todosCount: () => state.todos.length,
};

Mutation

実際に Vuex のストアの状態を変更できる唯一の方法。
ReduxだとReducerがやっている役割。

src/store/index.ts
// 状態の変化
export const ADD_TODO_TEXT = "ADD_TODO_TEXT";

// mutation関数の戻り値はvoidで固定。自由に決めることができるのは「関数名」と「payload」
interface IMutations {
  // 関数名:payloadの型

}
type Mutations<S, M> = {
  [K in keyof M]: (state: S, payload: M[K]) => void
}

// Vuexのストアの状態を変更できる唯一の方法
export const mutations: Mutations<State, IMutations> = {
  // 定数を関数名として使用できる ES2015 の算出プロパティ名(computed property name)機能を使用
  [ADD_TODO_TEXT](state, text) {
    const todo = {
      id: 0,
      text
    };
    if (state.todos.length !== 0) {
      todo.id = state.todos[state.todos.length - 1].id + 1;
    }
    state.todos.push(todo);
  },
};

Action

  • アクションは、状態を変更するのではなく、ミューテーションをコミットする。
  • アクションは任意の非同期処理を含むことができる。
// Actionはgetters・mutations・同じModuleの参照・Rootの参照を第一引数のcontextに持っている
interface IActions {
  // 関数名:payloadの型
  asyncSetTodoText: string;
}
// Actionsの戻り値は保留してanyに。async functionを指定でき、同期的に書いてもライブラリ中でPromiseとなるため、複雑になる。
type Actions<S, A, G = {}, M = {}, RS = {}, RG = {}> = {
  [K in keyof A]: (ctx: Context<S, A, G, M, RS, RG>, payload: A[K]) => any
}
type Context<S, A, G, M, RS, RG> = {
  commit: Commit<M>;
  dispatch: Dispatch<A>;
  state: S;
  getters: G;
  rootState: RS;
  rootGetters: RG;
}
// Mで渡ってくるIMutationのkeyofで定義されている関数名を特定する。
// keyof Mは '[ADD_TODO_TEXT]'
// 関数型直前に <T extends keyof M>と付与することでTはkeyof Mで定義されているいずれかしか入力できなくなる
// 第一引数に、これらいずれかの文字列が入力されたとき、第二引数の型がM[T]として確定する。
// Lookup Typesを利用して引数同士の関連付けを行っている。
type Commit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void;
type Dispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any;

// ミューテーションをコミットする。非同期処理を含むことができる。
export const actions: Actions<
  State,
  IActions,
  IGetters,
  IMutations
> = {
  asyncSetTodoText({ commit }, text) {
    commit(ADD_TODO_TEXT, text);
  },
};

実行

src/main.ts
import Vue from 'vue';
import App from './App.vue';
import store from './store'

store.dispatch('asyncSetTodoText', 'Hello World!');
store.dispatch('asyncSetTodoText', 'Hello World!!');

console.log('todos', store.getters.todos);
console.log('count', store.getters.todosCount);
new Vue({
  render: (h: (app: any) => Vue.VNode) => h(App),
}).$mount('#app');

ブラウザでアクセスして、consoleに表示されているか確認する。

この時点のソース

定義の整理

store/models/TodoItem.ts
export type TodoItem = {
  id: number;
  text: string;
};
store/types.ts
type Getters<S, G, RS = {}, RG = {}> = {
  [K in keyof G]: (state: S, getters: G, rootState: RS, rootGetters: RG) => G[K]
}
type Mutations<S, M> = {
  [K in keyof M]: (state: S, payload: M[K]) => void
}
type Commit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void;
type Dispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any;
type Context<S, A, G, M, RS, RG> = {
  commit: Commit<M>;
  dispatch: Dispatch<A>;
  state: S;
  getters: G;
  rootState: RS;
  rootGetters: RG;
}
type Actions<S, A, G = {}, M = {}, RS = {}, RG = {}> = {
  [K in keyof A]: (ctx: Context<S, A, G, M, RS, RG>, payload: A[K]) => any
}
store/todoTypes.ts
export interface State {
  todos: TodoItem[];
}
// getters向け、getter関数の戻り型を定義
export interface IGetters {
  todos: TodoItem[];
  todosCount: number;
}
export const ADD_TODO_TEXT = "ADD_TODO_TEXT";
// mutations向け、mutation関数のpayloadを定義
export interface IMutations {

}
// actions向け、action関数のpayloadを定義
export interface IActions {
  asyncSetTodoItemText: string;
}
src/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import { Getters, Mutations, Actions } from './types';
import { State, IGetters, IMutations, IActions, ADD_TODO_TEXT } from './todoType';

Vue.use(Vuex);

const todoState: State = ({ todos: [] } as State);

const getters: Getters<State, IGetters> = {
  todos: (state) => state.todos,
  todosCount: (state) => state.todos.length,
};

const mutations: Mutations<State, IMutations> = {
  [ADD_TODO_TEXT](state, text) {
    const todo = {
      id: 0,
      text
    };
    if (state.todos.length !== 0) {
      todo.id = state.todos[state.todos.length - 1].id + 1;
    }
    state.todos.push(todo);
  },
};

const actions: Actions<
  State,
  IActions,
  IGetters,
  IMutations
> = {
  asyncSetTodoItemText({ commit }, text) {
    commit(ADD_TODO_TEXT, text);
  },
};

export default new Vuex.Store({
  state : todoState,
  mutations,
  getters,
  actions,
});

この時点のソース

3. storeで保持したstateをViewで表示する

ルートインスタンスに store オプションを渡すことで、渡されたストアをルートの全ての子コンポーネントに注入する。
これは this.$store で各コンポーネントから参照することができる。

src/main.ts
new Vue({
  render: (h: (app: any) => Vue.VNode) => h(App),
+  store,
}).$mount('#app');

ToDoコンポーネントとToDoListコンポーネントを作る

Todoコンポーネントは、propとして渡されてきたtodoのtextを表示するだけ。

src/components/Todo.vue
<template>
  <li>{{text}}</li>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { TodoItem } from "../models/TodoItem";

@Component
export default class Todo extends Vue {
  @Prop()
  public todo: TodoItem;
  get text() {
    return this.todo.text;
  }
}
</script>

TodoListコンポーネントは、todosの各要素をTodoコンポーネントに渡す。
ここで、配列としてコンポーネントを複数生成するときkeyが必要になる。

src/components/TodoList.vue
<template>
  <ul class="todos">
    <Todo v-for="todo in todos" :key="todo.id" :todo="todo" />
  </ul>
</template>
<script lang="ts">
import { Prop, Component, Vue, Emit } from "vue-property-decorator";
import Todo from "./Todo.vue";
import { TodoItem } from "../models/TodoItem";

@Component({ components: { Todo } })
export default class TodoList extends Vue {
  @Prop()
  public todos: TodoItem[];
}
</script>

ブラウザに表示

Hellow Worldを表示させているだけのAppコンポーネントにTodoListコンポーネントを表示させる。
ここでstoreから値を取得してTodoListコンポーネントに与えている。

src/App.vue
<template>
  <div id="app">
    <TodoList :todos="todos" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import TodoList from "./components/TodoList.vue";

@Component({
  components: {
    TodoList
  }
})
export default class App extends Vue {
  get todos() {
    return this.$store.getters.todos;
  }
}
</script>

この時点のソース

4. フォームからtodoを追加

フォームからtodoを追加するために、AddTodoコンポーネントを作る。

src/components/AddTodo.vue
<template>
  <div>
    <input type="text" v-model="text" />
    <button @click="addTodo">Add Todo</button>
  </div>
</template>
<script lang="ts">
import { Prop, Component, Vue } from "vue-property-decorator";
@Component
export default class AddTodo extends Vue {
  @Prop({})
  public text: string;

  public addTodo() {
    this.$store.dispatch("asyncSetTodoText", this.text);
  }
}
</script>

AppコンポーネントにAddTodoコンポーネントを追加する。

<template>
  <div id="app">
    <AddTodo />
+    <TodoList :todos="todos" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import TodoList from "./components/TodoList.vue";
+ import AddTodo from "./components/AddTodo.vue";

@Component({
  components: {
    TodoList
+   AddTodo
  }
})
export default class App extends Vue {
  get todos() {
    return this.$store.getters.todos;
  }
}
</script>

この時点のソース

モジュール化

Vuexの機能でもう一つ、モジュールを使っていなかったので試してみる。
Vuex ではストアをモジュールに分割できるようになっている。

src/store/index.ts
- export default new Vuex.Store({
-    state: todoState,
-    mutations,
-    getters,
-    actions,
- });
+ const todoModule = {
+    namespaced: true,
+    state: todoState,
+    mutations,
+    getters,
+    actions,
+ }
+
+ export default new Vuex.Store({
+   modules: { todo: todoModule }
+ });

使う側では、名前空間を区切って使うようにする。

src/main.ts
-store.dispatch('asyncSetTodoText', 'Hello World!');
-store.dispatch('asyncSetTodoText', 'Hello World!!');
+store.dispatch('todo/asyncSetTodoText', 'Hello World!');
+store.dispatch('todo/asyncSetTodoText', 'Hello World!!');
console.log('todos', store.getters.todos);
console.log('count', store.getters.todosCount);
new Vue({
  render: (h: (app: any) => Vue.VNode) => h(App),
  store,
}).$mount('#app');
src/App.vue
 export default class App extends Vue {
   get todos() {
-    return this.$store.getters.todos;
+    return this.$store.getters["todo/todos"];
   }
src/components/AddTodo.vue
   public addTodo() {
-    this.$store.dispatch('asyncSetTodoText', this.text);
+    this.$store.dispatch("todo/asyncSetTodoText", this.text);
   }

この時点のソース

フォルダ整理

todoの定義をstore/todo/index.tsに移し替える。
todoTypes.tsもstore/todo/type.tsに合わせてリネーム。

src/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import todo from './todo';
// ルートコンポーネントに store オプションを指定することですべての子コンポーネントにストアを注入。
Vue.use(Vuex);

export default new Vuex.Store({
  modules: { todo },
});
store/todo/index.ts
import { Getters, Mutations, Actions } from '../types';
import {
  State, IGetters, IMutations, IActions, ADD_TODO_TEXT,
} from './types';


// 状態管理用state
const todoState: State = ({ todos: [] } as State);

// 値の取得
const getters: Getters<State, IGetters> = {
  todos: state => state.todos,
  todosCount: state => state.todos.length,
};

// Vuexのストアの状態を変更できる唯一の方法
const mutations: Mutations<State, IMutations> = {
  // 定数を関数名として使用できる ES2015 の算出プロパティ名(computed property name)機能を使用
  [ADD_TODO_TEXT](state, text) {
    const todo = {
      id: 0,
      text,
    };
    if (state.todos.length !== 0) {
      todo.id = state.todos[state.todos.length - 1].id + 1;
    }
    state.todos.push(todo);
  },
};

// ミューテーションをコミットする。非同期処理を含むことができる。
const actions: Actions<
  State,
  IActions,
  IGetters,
  IMutations
> = {
  asyncSetTodoText({ commit }, text) {
    commit(ADD_TODO_TEXT, text);
  },
};

export default {
  namespaced: true,
  state: todoState,
  mutations,
  getters,
  actions,
};

型定義ファイルをsrc/typesフォルダにまとめる。
また、vuexのStoreの拡張を行う。
vscodeで行ったが、定義後再起動しないと型が反映されないトラブルがあった。

src/types/shims-vuex.d.ts
import 'vuex';
import * as Todo from '../store/todo/types';

declare module 'vuex' {
  type Getters<S, G, RS = {}, RG = {}> = {
    [K in keyof G]: (state: S, getters: G, rootState: RS, rootGetters: RG) => G[K]
  }
  type Mutations<S, M> = {
    [K in keyof M]: (state: S, payload: M[K]) => void
  }
  type ExCommit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void;
  type ExDispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any;
  type ExActionContext<S, A, G, M, RS, RG> = {
    commit: ExCommit<M>;
    dispatch: ExDispatch<A>;
    state: S;
    getters: G;
    rootState: RS;
    rootGetters: RG;
  }
  type Actions<S, A, G = {}, M = {}, RS = {}, RG = {}> = {
    [K in keyof A]: (ctx: ExActionContext<S, A, G, M, RS, RG>, payload: A[K]) => any
  }
  type RootGetters = Todo.RootGetter; // 増えたら、& Hoge.RootGetter のように、Intersection Typesで連結していく。
  interface ExStore extends Store<{}> {
    // ここに拡張型を追加していく。
    getters: RootGetters
  }
}
src/App.vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
+ import * as Vuex from 'vuex';
import TodoList from './components/TodoList.vue';
import AddTodo from './components/AddTodo.vue';

@Component({
  components: {
    TodoList,
    AddTodo,
  },
})
export default class App extends Vue {
+  $store!: Vuex.ExStore;

  get todos() {
    return this.$store.getters['todo/todos'];
  }
}
</script>

この時点のソース

次回

Todoの完了・未完了を切り替える「Toggle Todo」機能を実装。

参考

Redux ExampleのTodo Listをはじめからていねいに
Redux ExampleのTodo Listをはじめからていねいにtypescriptで
MithrilのTodo Listをはじめからていねいに
実践TypeScript
webpackのビルド高速化の効果を測ってみた
TypeScriptでVueを書く
vue.js todo mvc
vuex
vue cli TypeScript のサポート
Vue + Vuex を使ってみた感想と、Redux との比較
Vue.js と TypeScript で Todo リストアプリを実装した
【Nuxt.js】Todoリストで理解するTypeScriptでVuex入門
Vue.js+TypeScriptで外部APIを使ったTODOリストを作ってみた
Vue.js + Vuexでデータが循環する全体像を図解してみた

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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