TypeScript
vue.js

Vue CLI 3.0 で TypeScript な Vue.js プロジェクトをつくってみる

この記事について

ついに Vue CLI 3.0 がリリースされました :tada:

Vue CLI 3.0 is here! – The Vue Point – Medium

Vue CLI 3.0 で Vue.js in TypeScript なプロジェクトをさくっとつくってみたので、その記録です。

3.0 では、プロジェクト作成時に TypeScript を選べるようになっており、Vue.js in TypeScript での開発に必要な諸々をまるっと入れてくれるので、新規開発で TypeScript を導入するハードルがグッと下がってるんじゃないか、と期待が膨らみます。

概要

  • Vue CLI: 3.0.0
  • TypeScript: 3.0.0
  • webpack: 4.16.5

Vue CLI 3.0 でプロジェクト作成すると TypeScript と webpack は上記のバージョンがインストールされます。

ドキュメントはこちらです。

Overview | Vue CLI 3

手順

1. Vue CLI 3 のインストール

手順はこちらに載っています( 2.x とはパッケージが別で、 @vue/cli となっているので注意が必要です)。

Installation | Vue CLI 3

$ yarn global add @vue/cli

私は、ひとまずグローバルに入れたくなかったので、適当なディレクトリを切ってそこに入れました。

$ vue -V
3.0.0

2. プロジェクトをつくる

$ vue create vue-ts-example

(プロジェクト名は適宜置き換えてください)

image.png

今回は TypeScript で書きたいので、下の「Manually select feature」を選択します。

Enter/Return すると、選択肢が出てきます。

image.png

お好みでオン/オフしてください。

以下、"class-style component syntax" を有効にしたパターンと無効にしたパターンに分けて記載します。

2.1. Use class-style component syntax? Yes にしたパターン

image.png

いくつか質問されますが、基本的にデフォルトでいいと思います。

package.json の中身

package.json
  "dependencies": {
    (snip)
    "vue-class-component": "^6.0.0",
    "vue-property-decorator": "^7.0.0",
    (snip)

この2つのパッケージが、 class-style component に必要なものです。

HelloWorld.vue の中身

デコレータを使って、クラス定義ができるようになっています。

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

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

2.2. Use class-style component syntax? No にしたパターン

package.json の中身

Yes にしたプロジェクトのと diff を取ると

14a15,16
>     "vue-class-component": "^6.0.0",
>     "vue-property-decorator": "^7.0.0",

この2つのパッケージがないことが分かります。

HelloWorld.vue の中身

シンプルに Vue.extend しています。

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
});
</script>

3. ファイル構成

デフォルトでは以下のようになっています。

$ tree -L 2
.
├── README.md
├── babel.config.js
├── cypress.json
├── jest.config.js
├── node_modules
(...)
├── package.json
├── public
│   ├── favicon.ico
│   ├── img
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.vue
│   ├── assets
│   ├── components
│   ├── main.ts
│   ├── registerServiceWorker.ts
│   ├── router.ts
│   ├── shims-tsx.d.ts
│   ├── shims-vue.d.ts
│   ├── store.ts
│   └── views
├── tests
│   ├── e2e
│   └── unit
├── tsconfig.json
├── tslint.json
└── yarn.lock

まず目を引くのが、 build, config といったディレクトリがなくなっていることです。

デフォルトの webpack の設定は、 @vue スコープのパッケージに分散して置かれており、 node_modules/@vue の下にある cli-plugin-* とか cli-service とかで別々に定義されて webpack-chain という仕組みで連結されているようです(詳細は以下のドキュメントをご覧ください)。

Working with Webpack | Vue CLI 3

他にも色々変わってて、たとえば、サーバ実行のコマンドが

package.json
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",

から

package.json
    "serve": "vue-cli-service serve",

に変わってたり、

src/registerServiceWorker.js とかいうファイルが追加されてたり。

今回の趣旨は TypeScript なので詳しくは割愛します。

4. TypeScript 書いていく

簡単なカウンターを実装していきます。

コンポーネントの記述については "Use class-style component syntax?" に Yes と答えたパターンと No と答えたパターンがあります。

4.1. Template

<template>
    <div>
      <p>{{ counter }}</p>
      <button @click="handleClick">Up</button>
    </div>
  </div>
</template>

4.2. Component

4.2.1. Use class-style component syntax? Yes にしたパターン

@Mutation デコレータを使いたかったので、vue-class パッケージを追加しました。

kaorun343/vue-property-decorator: Vue.js and Property Decorator

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import { mapGetters, mapMutations } from 'vuex'
import { Mutation } from 'vuex-class'

@Component({
  computed: {
    ...mapGetters({ counter: 'current' })
  },
  methods: {
    ...mapMutations(['increment'])
  }
})
export default class HelloWorld extends Vue {
  @Prop() private value!: number
  @Mutation('increment') increment!: () => void

  handleClick () {
    this.increment()
  }
}
</script>

4.2.2. Use class-style component syntax? No にしたパターン

<script lang="ts">
import Vue, { ComponentOptions } from 'vue';
import { mapGetters, mapMutations } from 'vuex'

interface HelloWorldComponent extends Vue {
  counter: number,
  increment: () => void
}

export default {
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  computed: {
    ...mapGetters({ counter: 'current' })
  },
  methods: {
    ...mapMutations(['increment']),
    handleClick () {
      this.increment()
    }
  }
} as ComponentOptions<HelloWorldComponent>

4.3. Store

Store は interface つくってから、オブジェクトに型を割り当てることもできますが、今回は直接 GetterTree<S, R> などの Vuex で定義されている型を指定しました。

import Vue from 'vue';
import Vuex, { StoreOptions, GetterTree, MutationTree, ActionTree } from 'vuex';

Vue.use(Vuex);

interface CounterState {
  counter: number
}

const state: CounterState = {
  counter: 1
}

const getters: GetterTree<CounterState, CounterState>  = {
  current (state: CounterState): number {
    return state.counter
  }
}

const mutations: MutationTree<CounterState> = {
  increment (state: CounterState): void {
    state.counter++
  }
}

const actions: ActionTree<CounterState, CounterState> = {}

const store: StoreOptions<CounterState> = {
  state,
  getters,
  mutations,
  actions,
}

export default new Vuex.Store<CounterState>(store)

4.4. Router

Router も TS で書かれてましたが、今回は手付かずです(そのまま載せておきます)。

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
    },
  ],
});

おわりに

コマンド叩いてポチポチするだけであまりに簡単に Vue.js in TypeScript なプロジェクトの雛形ができてしまって感動しました。

短時間で雑に書いたので、間違いがあるかもしれません。何かあればコメント欄にてご指摘いただけると助かります :bow: