maesblog

TypeScriptでVue.jsを書く – Vue CLIを使った開発のポイントを紹介

2018年8月11日に新しいメジャーバージョンとなるVue CLI 3.0がリリースされました。Vue CLI 3では、公式にTypeScriptをサポートし、 TypeScriptを利用するプロジェクトの生成に対応しました。これにより、TypeScriptでVueを気軽に試せるようになったということで、今回ちょっとどんなものか試してみることにしました。試した中でポイントとなりそうな部分をまとめてみたので紹介します。なお、私は普段はAngularでTypeScriptを書いているため、TypeScriptのクラス構文を使った説明になっています(クラス構文を使わなくても、TypeScriptでvue.jsを書くことはできます)。

はじめに – Vue CLI + TypeScript

Vue CLIとは

Vue CLIは、Vueの開発環境を構築するためのツールです。コマンド一発で、複雑なフロントエンドの開発環境を作ってくれるもので、AngularReactでも同様のツールがあり、この類のツールは、昨今のフロントエンドの開発現場においては、欠かすことのできないツールのひとつとなっています。Vue CLIは対話形式で自分の好きなツールを選んで開発環境をカスタマイズすることもでき自由度は高いです。さらにVue CLI 3では、GUIでの操作にも対応したり、バージョンアップを重ねる度にいろいろと機能が充実してきています

TypeScriptでVueを書くメリット

今回のVue CLI 3では、TypeScriptで開発するプロジェクトの生成にも対応しました。Vue CLI 3自体は、今年の1月ごろからα版がリリースされているので、もうすでに試されているかもしれませんが、まだであればぜひ試していただきたい機能です。TypeScriptを使うことで、以下のような利点があるかなと思っています。

  • これまでのVue.js特有のオブジェクト構文で書いた時より、TypeScriptのクラス構文で書くことでコード量を少なくできる
  • propsやdata、methodsなどのオプションそれぞれを区別して書けるようになるため、それぞれの役割が明確になり、全体的な見通しも良くなる
  • TypeScriptの型システムによりコンパイル時に変数の使われ方のチェックが行われることで、未然にエラーや凡ミスを防げる
  • TypeScriptの型情報があることで、VS Codeなどのエディタで、オブジェクトのプロパティやメソッドの候補を出してくれたり、コードの自動補完を行ってくれる
  • TypeScriptのクラス構文でVueを書くとほぼAngularと同じような構文になるので、Angularもそれほど苦労せずに書けるようになる

もちろんTypeScriptを使いこなすまでにはそれなりの学習が必要です。ただ、普段JavaScript(特にES2015+)を書いていれば、そこまで大きな違いはないので、まずは変数に型を指定するところから始めて、少しずつ知識を増やしていけばいいんじゃないかなと思っています。書き始めるとその便利さがすぐにわかるようになるかと思います。

TypeScript + Vue サンプル

今回のこの記事を書く上でシンプルなサンプルを作成しました。テキストフィールドに入力し、ボタンを押すと、Helloの後にアッパーケースに変換して表示するだけのものです。

別ウィンドウで開く

実際にVue CLIでプロジェクトを作成し、TypeScriptでコードを書き、TypeScriptとJestでユニットテストコードを書き、最終的にビルドを行っています。その中でポイントとなると思った部分をこれから紹介していきます。

なお、このサンプルのソースコードはGitHubにアップしていますので、合わせてこちらも確認してみてください。

Vue CLIでTypeScriptプロジェクトを生成する

それでは、さっそくVue CLIを使ってTypeScriptの開発環境を構築するところから説明していきます。

Vue CLIのインストール

お使いの環境にVue CLIがインストールされていない場合は、以下のコマンドでインストールします。

# npmでインストールする場合
$ npm install -g @vue/cli

# yarnでインストールする場合
$ yarn global add @vue/cli

インストールが完了したら、以下のコマンドで問題なくVue CLIがインストールされているか確認します。ちゃんとバージョン情報が表示されたらインストールされていることになります。

$ vue --version
3.0.0

Vue CLI 3の詳細については、やはり公式のドキュメントが詳しいと思うので、まずは一通り目を通されることをお勧めします。

TypeScriptプロジェクトを新規作成

それでは、次にインストールしたVue CLIを使って、TypeScriptを使ったVueプロジェクトを作成します。任意のディレクトリに移動して、以下のコマンドを実行します。引数にはプロジェクト名を指定します(当記事では、vue-typescript-sampleとしています)。

$ vue create vue-typescript-sample

コマンドを実行すると、以下のテキストが表示されます。最初は「default (babel, eslint)」が選択されていますが、「Manually select features」を選択して、returnキーを押します。プロジェクトで使用するツールを手動で選択できるようになります。

Vue CLI v3.0.0
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features 

引き続き、対話形式での設定が続きます(スペースで選択、aで全選択/全解除、iで選択状況を反転)。ここで大事なことは、「Check the features needed for your project」のところでTypeScriptを選択することと、「Use class-style component syntax」のところでYesを選択することです。VueをTypeScriptかつクラス属性で書くためのツールをインストールしてくれます。それから「Pick a unit testing solution」のところをJestとしてください(記事の後半で説明するユニットテストをJestで書いています)。その他の部分は、今回特に触れていないので、お好きなように選んでも構いません。私は以下のように選択しました。

Vue CLI v3.0.0
? Please pick a preset: Manually select features
? Check the features needed for your project: TS, Vuex, CSS Pre-processors, Linter, Unit
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): SCSS/SASS
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

ローカルサーバーを起動して確認

最後まで選択が終わると、Vue CLIによるプロジェクトの作成が始まります。プロジェクトが作成されたら、プロジェクトディレクトリに移動して、ローカルサーバを起動してみましょう。

$ cd vue-typescript-sample
$ npm run serve

ブラウザで「http://localhost:8080/」にアクセスしたら、以下の画面が表示されます。

vue.js + Typescript

一通り生成されたpackage.jsonファイルや、ディレクト構成、ソースコードなどを見てみると良いでしょう。TypeScriptに特有のものが確認できるかと思います。

TypeScriptプロジェクトに特有のもの

TypeScript本体はもちろん、ts-loader(webpack用)、TSLint(Linttツール)、ts-jest(jest連携用)などさまざまなTypeScript関連のツールなどがインストールされ、生成さます。その中で特に覚えておきたいものを紹介します。

  • vue-property-decorator

    VueをTypeScript特有のクラス構文で書くためのツールです。Vue CLIでプロジェクトを作成する時に、「Use class-style component syntax」をyesと選ぶとインストールされます。若干詳しく説明すると、ラップしているvue-class-componentによってクラス構文で書けるようになっていて、このvue-property-decoratorによってさまざまなデコレータが使えるようになっています。

  • shims-vue.d.tsファイル

    これは結構大事なファイルです。Vueを書くときに単一ファイルコンポーネント(SFC)形式で書くことが多いかと思います。このSFCの拡張子は.vueとなっているので、普通に考えるとTypeScriptファイルとしては扱われません。そこで.vueファイルをimportする時に、記述されているコードをTypeScriptとして認識させる役割を担っているのがこのshims-vue.d.tsファイルです。なお、このshims-vue.d.tsファイルは、.vueファイルの中にimportするとか何か特別なことをする必要はありませんsrcディレクトリ内にあれば問題ありません。

  • tsconfig.jsonファイル

    TypeScriptのコンパイル時に使われる設定ファイルです。Vue CLIでのプロジェクト生成時に十分な設定がなされているので、あまりいじる必要はないかと思いますが、TypeScriptを書く上で何かあった場合に必要になるので、頭の片隅に入れておくと良いでしょう。

TypeScriptでVueを書く

この章の後にVueをTypeScriptで書く際のポイントを紹介しますが、まずは通常のJavaScriptで書いたものと、TypeScriptで書いたものを見比べてもらえればと思います。この記事の最初で紹介したサンプルのコードです。SFCのscript要素内に書いたコードを抜き出しました。

【Before】Vueを「JavaScript」で書いた場合

以下は、そのままJavaScriptで書いた構文です。なるべくいろんな事例が紹介できるように冗長に書いています(コードについての詳細は省きます。コード内のコメントを見てください)。

export default {
  /** コンポーネント名 */
  name: 'hello-vue',

  /** 親コンポーネントから受け取るデータ */
  props: ['val'],

  /** コンポーネントに属するデータ(状態) */
  data() {
    return {
      value: this.val,
      inputValue: '',
    };
  },

  /**
   * lifycycle hooks
   * mountedはコンポーネントがマウントされる直前に呼び出される
   */
  mounted() {
    console.log('mounted');
  },

  /** 
   * いわゆるGet/Setアクセサを定義
   */
  computed: {
    /** ボタンが押せるかどうか取得する */
    isDisabled() {
      return this.inputValue === '';
    },
  },

  /**
   * コンポーネント内で使用するメソッドを定義
   */
  methods: {
    /** テキストが入力された時に呼び出される */
    handleInput($event) {
      this.inputValue = $event.target.value;
    },
    /** ボタンがクリックされた時に呼び出される */
    handleClick() {
      if (this.inputValue === '') {
        return;
      }
      this.value = this.inputValue;
      this.inputValue = '';

      /** カスタムイベントのemit */
      this.$emit('handle-click', this.value);
    },
  },

  /*
   * dataの更新を監視し、変更時に呼び出されるメソッドを定義
   */
  watch: {
    /** 
     * data.valueの値が変更される度に呼び出される
     * 引数に、変更後と変更前の値が渡される
     */
    value(newValue, oldValue) {
      console.log(`watch: ${newValue}, ${oldValue}`);
    },
  },

  /**
   * テンプレートの出力の際に任意のフォーマットに変換するためのメソッド
   */
  filters: {
    /** 渡された値をアッパーケースに変換する */
    convertUpperCase(value) {
      if (!value) return;
      return value.toUpperCase();
    },
  },
};
HelloVue.vue(JavaScript版)

【After】Vueを「TypeScript」で書いた場合

上記のJavaScriptの構文をTypeScriptで書き換えると以下のようになります。クラス構文で書くことによって、それぞれのオプションを区別して書くことができ、さらにオブジェクトリテラルの常にカンマで繋がれた状態からも解放されて、見通しが良くだいぶスッキリしてます。

import { Component, Prop, Emit, Watch, Vue } from 'vue-property-decorator';

@Component({
  /** filters */
  filters: {
    convertUpperCase(value: string): string | null {
      if (!value) {
        return null;
      }
      return value.toUpperCase();
    },
  },
})
export default class HelloVue extends Vue {
  /** props */
  @Prop() val!: string;

  /** data */
  value: string = this.val;
  inputValue: string = '';

  /** emit */
  @Emit('handle-click')
  clickButton(val: string): void {}

  /** watch */
  @Watch('value')
  onValueChange(newValue: string, oldValue: string): void {
    console.log(`watch: ${newValue}, ${oldValue}`);
  }

  /** computed */
  get isDisabled(): boolean {
    return this.inputValue === '';
  }

  /** lifecycle hook */
  mounted(): void {
    console.log('mounted');
  }

  /** methods */
  handleInput($event: Event): void {
    this.inputValue = (($event.target as any) as HTMLInputElement).value;
  }
  handleClick(): void {
    if (this.inputValue === '') {
      return;
    }
    this.value = this.inputValue;
    this.inputValue = '';
    this.clickButton(this.value);
  }
}
HelloVue.vue(TypeScript版)

私個人的には、普段仕事でAngularを書いていることから、やはりTypeScriptで書いたこちらの構文がしっくりきています(ほぼAngularじゃないかというツッコミがきそうですね)。

TypeScriptでVueを書くときのポイント

上記のJavaScriptとTypeScriptで書いたコードの比較を踏まえて、以下にTypeScriptでVueを書く際のポイントを紹介します。

  1. 「data」は クラスのプロパティ になる

    dataは、コンポーネント(Vueインスタンス)の状態を保持するためのものです。JavaScriptで書く場合(Vue.extend()内で書く場合)は、data関数の戻り値としてオブジェクトで定義しました。TypeScriptによるクラス構文で書く場合は、それぞれクラスのプロパティとして定義します。

    /** JavaScriptで書く場合 */
    data() {
      return {
        foo: 'foo',
        bar: 123,
      };
    },
    
    /** TypeScriptで書く場合 */
    foo: string = 'bar';
    bar: number = 123;
    
  2. 「methods」は クラスのメソッド になる

    methodsは、コンポーネント内で使用するメソッドです。JavaScriptで書く場合は、使用するメソッドをmethodsフィールドのオブジェクトの中に定義しました。TypeScriptによるクラス構文で書く場合は、それぞれクラスのメソッドとして定義します。

    /** JavaScriptで書く場合 */
    methods: {
      foo(a) {
        return a;
      },
      bar(b) {
        return b;
      },
    },
    
    /** TypeScriptで書く場合 */
    foo(a: string): string {
      return a;
    }
    bar(b: boolean): boolean {
      return b;
    }
    
  3. 「computed」は get / setアクセサ になる

    computedは、いわゆるgetter / setterとして使用するものです。JavaScriptで書く場合は、computedフィールドのオブジェクトの中に、getter / setter となる関数を定義しました。TypeScriptによるクラス構文で書く場合は、get / setアクセサを使って定義します。

    /** JavaScriptで書く場合 */
    computed: {
      foo() {
        get() {
          return this.foo;
        }
        set(v) {
          this.foo = v;
        }
      },
    },
    
    /** TypeScriptで書く場合 */
    get foo(): string {
      return this._foo;
    }
    set foo(v: string): void {
      this._foo = v;
    }
    
  4. 「データのinput/output」は @props / @emit で書く

    コンポーネント間でデータをinputする時はprops、outputする時は$emitを使い、propspropsフィールドに配列として定義し、$emitはカスタムイベントを発火したい場所で呼び出すように定義しました。TypeScriptによるクラス構文で書く場合は、vue-property-decoratorで用意されているデコレータ@Props@Emitを使って定義します。

    /** JavaScriptで書く場合 */
    props: ['foo'],
    
    methods: {
      bar() {
        this.$emit('barEvent', 'bar')
      }
    },
    
    /** TypeScriptで書く場合 */
    @Prop() foo!: string;
    
    @Emit('barEvent')
    bar(val: string): void {}
    
    bazz() {
      this.bar('bar')
    }
    
  5. 「Lifecycle hook」は クラスのメソッド で書く

    Lifecycle hookはコンポーネントの、さまざまなライフサイクルのタイミングで呼び出されるメソッドです。例えば、mounted()であれば、コンポーネントがマウントされる直前に呼び出されます。JavaScriptで書く場合は、使用したいLifecycle hookをそのまま関数として定義しました。TypeScriptによるクラス構文で書く場合も同様に、そのままクラスのメソッドとして定義します。

    /** JavaScriptで書く場合 */
    mounted() {
      return 'foo';
    },
    
    /** TypeScriptで書く場合 */
    mounted(): string {
      return 'mounted';
    }
    
  6. 「vue-property-decoratorで用意されているもの」は デコレーター で書く

    上記でも紹介したprops$emitもそうですが、さらにdataの任意の値が変更される度に呼び出されるwatchなどもvue-property-decoratorで用意されているデコレータ@Watchを使って定義することができます。vue-property-decoratorで用意されているものは、極力デコレータを使って書くとよいでしょう。コードの見通しがよくなります。

    /** JavaScriptで書く場合 */
    watch: {
      foo(newFoo, oldFoo) {
        return newFoo;
      },
    },
    
    /** TypeScriptで書く場合 */
    @Watch('foo')
    onFooChange(newFoo: string, oldFoo: string): string {
      return newFoo;
    }
    
  7. 「デコレータを使って書けないもの」は @component内にそのまま 書く

    テンプレートの出力の際に任意のフォーマットに変換する際に使用するfiltersは、this.xxxの形で使用することがないので、vue-property-decoratorでデコレータが用意されていません。この類のものは、特にTypeScript特有の書き方が用意されておらず、JavaScriptと同じ書き方で定義します。その場合は、@Componentデコレータ内で定義することになります。

    /** JavaScriptで書く場合 */
    filters: {
      foo(value) {
        return value.toUpperCase();
      },
    },
    
    /** TypeScriptで書く場合 */
    @Component({
      filters: {
        foo(value: string): string {
          return value.toUpperCase();
        },
      },
    })
    
  8. script要素に lang="ts"属性 をつける

    SFC形式による.vueファイル内のScript要素の中にTypeScriptを書く場合は、script要素にlang="ts"属性をつけるようにします。上記したように、shims-vue.d.tsファイルによって、script要素内に書いたコードがTypeScriptとして認識されるようになります

    <template>
    <!-- Templateを書く -->
    </template>
    
    <script lang="ts">
    /** TypeScriptを書く */
    </script>
    
    <style>
    /** Styleを書く */
    </style>
    

その他TypeScriptでVueを書く際の詳細については、以下などを参考にしてください。

TypeScriptでユニットテストを書く

Vue CLIで生成したプルジェクトは、ユニットテストのコードもTypeScriptで書くことができます。TypeScriptでのユニットテスト(単体テスト)についても軽く触れておきます。プロジェクト生成時に「Mocha + Chai」か「Jest」を選択することができます。どちらを選んでもTypeScriptでユニットテストを書くことができます(Jestの場合、プロジェクト生成時にts-jestがインストールされます)。

今回は、スナップショットテストができること、ブラウザを使わずコマンドベースでテストを実行できることからJestを選んでいます(Jestについてはここでは詳しく説明しないので、Jestの公式ドキュメント当ブログの記事などを参考にしてください)。

また、公式のドキュメントで紹介されている以下の記事も紹介しておきます。

TypeScriptで書いたユニットテストコード

以下は、上記のサンプルコードに対するユニットテストをTypeScriptで書いたものです(コードについての詳細は省きます。コード内のテストケースの記述などを見てください)。なんとなく感じはつかめるかと思います。

import { Wrapper, shallowMount } from '@vue/test-utils';
import HelloVue from '@/components/HelloVue.vue';

describe('HelloVue.vue', () => {
  /** ラッパー変数の宣言 */
  let wrapper: Wrapper;

  it('propsで受け取る値のテスト', () => {
    // 準備
    const val = 'Vue';
    wrapper = shallowMount(HelloVue, {
      propsData: { val },
    });

    // 検証
    expect(wrapper.props().val).toBe(val);
    expect(wrapper.text()).toMatch(`Hello VUE`);
  });

  it('描画されるDOMのテスト', () => {
    // 準備
    wrapper = shallowMount(HelloVue);

    // 検証
    expect(wrapper.contains('h1')).toBeTruthy();
    expect(wrapper.contains('input')).toBeTruthy();
    expect(wrapper.contains('button')).toBeTruthy();
  });

  it('ボタンの非活性のテスト', () => {
    // 準備
    wrapper = shallowMount(HelloVue);
    const button = wrapper.find('button');

    // 実行
    wrapper.setData({ inputValue: '' });

    // 検証
    expect(button.element.getAttribute('disabled')).toBeTruthy();
  });

  describe('イベントのテスト', () => {
    beforeEach(() => {
      wrapper = shallowMount(HelloVue);
    });

    it('テキスト入力時にhandleInputが呼ばれるかテスト', () => {
      // 準備
      const spy = jest.spyOn(wrapper.vm, 'handleInput');

      // 実行
      wrapper.find('input').trigger('input');

      // 検証
      expect(spy).toHaveBeenCalled();
    });

    it('ボタン押下時にhandleClickが呼ばれるかテスト', () => {
      // 準備
      const spy = jest.spyOn(wrapper.vm, 'handleClick');
      wrapper.setData({ inputValue: 'AAA' });

      // 実行
      wrapper.find('button').trigger('click');

      // 検証
      expect(spy).toHaveBeenCalled();
    });

    it('入力なしの状態でhandleClickが呼ばれないかテスト', () => {
      // 準備
      wrapper = shallowMount(HelloVue);
      const spy = jest.spyOn(wrapper.vm, 'handleClick');
      wrapper.setData({ inputValue: '' });

      // 実行
      wrapper.find('button').trigger('click');

      // 検証
      expect(spy).not.toHaveBeenCalled();
    });
  });

  describe('watcherのテスト', () => {
    it('valueの値が変更された時にwatchが機能するかテスト', () => {
      // 準備
      wrapper = shallowMount(HelloVue, {
        propsData: { val: 'AAA' },
      });
      const spy = jest.spyOn(console, 'log');

      // 実行
      wrapper.setData({ value: 'BBB' });

      // 検証
      expect(wrapper.vm.value).toBe('BBB');
      expect(spy).toHaveBeenCalledWith('watch: BBB, AAA');

      spy.mockClear();
    });
  });

  describe('Lifecycle Hookのテスト', () => {
    it('マウント時に mountedが機能するかテスト', () => {
      // 準備
      const spy = jest.spyOn(console, 'log');

      // 実行
      shallowMount(HelloVue);

      // 検証
      expect(spy).toHaveBeenCalled();

      spy.mockClear();
    });
  });

  describe('computedのテスト', () => {
    beforeEach(() => {
      wrapper = shallowMount(HelloVue);
    });

    it('isDisabledがtrueを返すかテスト', () => {
      // 実行
      wrapper.setData({ inputValue: '' });
      const disabled = wrapper.vm.isDisabled;

      // 検証
      expect(disabled).toBeTruthy();
    });

    it('isDisabledがfalseを返すかテスト', () => {
      // 実行
      wrapper.setData({ inputValue: 'AAA' });
      const disabled = wrapper.vm.isDisabled;

      // 検証
      expect(disabled).toBeFalsy();
    });
  });

  describe('methodのテスト', () => {
    beforeEach(() => {
      wrapper = shallowMount(HelloVue);
    });

    it('handleInputメソッドのテスト', () => {
      // 準備
      const event = {
        target: { value: 'AAA' },
      } as any;

      // 実行
      wrapper.vm.handleInput(event);

      // 検証
      expect(wrapper.vm.inputValue).toBe('AAA');
    });

    it('handleClickメソッドのテスト', () => {
      // 準備
      wrapper.setData({ inputValue: 'AAA' });
      const spy = jest.spyOn(wrapper.vm, '$emit');

      // 実行
      wrapper.vm.handleClick();

      // 検証
      expect(wrapper.vm.value).toBe('AAA');
      expect(wrapper.vm.inputValue).toBe('');
      expect(spy).toHaveBeenCalledWith('handle-click', 'AAA');
    });
  });

  describe('filtersのテスト', () => {
    it('アッパーケースに変換されるかテスト', () => {
      // 準備
      wrapper = shallowMount(HelloVue, {
        propsData: { val: '' },
      });
      wrapper.setData({ value: 'Bbb' });

      // 実行
      const received = wrapper.find('h1').text();

      // 検証
      expect(received).toBe('Hello BBB');
    });
  });

  describe('スナップショットテスト', () => {
    it('HelloVueテンプレートのスナップショット', () => {
      // 準備
      wrapper = shallowMount(HelloVue);

      // 検証
      expect(wrapper.html()).toMatchSnapshot();
    });
  });
});
HelloVue.spec.ts

TypeScriptでユニットテストを書くときのポイント

基本的には、JavaScriptで書く時とほとんど変わらず書くことができるかと思います。ほとんど型推論を利用して書いています。なお、最初のwrapper変数の宣言の部分で、let wrapper: Wrapper<HelloVue>;といったようにHelloVue型(HelloVueは、私がクラス構文で書くときにつけたクラスの名前)を指定しています。エディタによるコードの補完が効いて、テストも書きやすくなります。

ただVS Codeを使用している場合、このままだと「HelloVueで定義しているメソッドやプロパティが存在しない」といったエラーが出てしまいます。VS Codeの.vueファイルを扱うための機能拡張Veturを入れていても、HelloVueクラスを型として認識してくれないようです。ということで、別途自分で型定義ファイルを用意する必要があります。その際に、vuetypeというツールが使えます。型定義ファイルを作ってくれるツールです。

vuetypeの使い方

以下のコマンドでプロジェクトディレクトリ内にインストールします。

$ npm i -D vuetype

package.jsonのscriptフィールドに登録して、npmコマンドとして使えるようにします。型定義ファイルを作りたいファイルの置き場所(src/components)を引数として指定します。

"scripts": {
  "vuetype": "vuetype src/components",
  "vuetype:w": "vuetype -w src/components"
},
package.json

以下のコマンドを実行すると、型定義ファイル(xxx.d.tsファイル)が同じディレクトリ内に作られます。

$ npm run vuetype

# 保存状態を監視して実行
$ npm run vuetype:w

型定義ファイルが作られたら、後は何もする必要はありません。VS Codeでのエラーも出なくなります。

スナップショットテスト

スナップショットテストは、レンダリングされたコンポーネント(テンプレート)の状態をスナップショットxxx.spec.ts.snapファイル)として保存し、テストを実行する度に前後のスナップショットを比較して、違いがないか(リグレッション)をチェックするためのテストです。Jestの代名詞ともなっています。詳細は、Jestの公式ドキュメント当ブログの記事などを参考にしてみてください。

なお、ここでは、スナップショットテストで、登録済みのスナップショットファイルを更新する方法を紹介しておきます。テンプレートの内容を修正すると、スナップショットテストでは前後の状態に差異があったとみなし、エラーとなります。これが意図的な修正であれば、スナップショットを最新の状態に更新する必要があります。Jestをwatchモードで起動すると、対話形式でスナップショットの更新を行えるようになりますpackage.jsonファイルのscriptフィールドに以下のコマンドを登録しておくと良いでしょう。

"scripts": {
  "test:w": "vue-cli-service test:unit --watch"
},
package.json

TypeScriptで書いたコードをビルドする

TypeScriptで書いたコードも、通常と同じく以下のコマンドでビルドできます。

$ npm run build

ビルドが完了すると、distディレクトリ内にバンドルファイルが出力されます。出力されたファイル一式を任意のサーバにデプロイすれば、一般への公開も可能です。

ところで、私の環境では、ビルドしたファイルをブラウザで開いて確認しようとしたところ、以下のようなエラーが出ました。

Refused to apply style from 'http://127.0.0.1:5500/css/app.2f562e5f.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
Failed to load resource: the server responded with a status of 404 (Not Found)
...

バンドルファイルがうまく読み込まれていないようでした。いろいろ調べたところ次の方法で解決しました。プロジェクトディレクトリ内にvue.config.jsファイルを新たに作成し、以下を記述しました。'./'の部分は''でも良さそうです。

module.exports = {
  baseUrl: './',
};
vue.config.js

今回のサンプルをGitHub Pagesとしてアップしてみました。

まとめ

以上、長々と書いてきましたが、Vue CLIを使えばTypeScriptでVueを書く環境がすぐに作れて、TypeScriptでコードを書き、ユニットテストを書き、デプロイまでできるようになります。紹介してきたようにTypeScriptでのクラス構文を使うと、コード量は少なくなり、メソッドやプロパティそれぞれ役割が明確になることで全体的な見通しも良くなり、さらにはTypeScriptによる型の恩恵も十分に受けられるようになります。特にVuexを使うようになると、mutationsで値を扱う際などTypeScriptの型が役に立つ機会がどんどん増えてくるかと思います。

個人的には、VueをTypeScriptで書くと、ほぼほぼAngularStencilで書くコードと同じじゃないかとツッコミを入れたくなりますが、逆に言うとAngularやReact派の自分でも、TypeScriptで書けるなら、Vueで開発するのもありかなと思わせてくれます。なので、Vue派の方もTypeScriptで書く習慣を身につけて、クラススタイルの構文に慣れてしまえば、今後仮にAngularへ転向することがあったとしてもそれほど苦労せずに済むはずです。今回リリースされたVue CLI 3で、TypeScriptの導入も簡単に行えるようになりました。この機会に、ぜひTypeScriptをお試しください。

Vuexについても触れたかったのですが、ちょっとこの記事が長くなりすぎるので、次回に取っておきます。

今回の記事で扱ったサンプルのコードはGitHubにアップしていますので、改めて最後に紹介しておきます。

それからちょうどこの記事を書いている時に、新しいVueの書籍が出るという情報があったので紹介しておきます。日本でのVueの第一人者的な存在の@kazuponさんやこの記事でも紹介したvuetypeの作者であり、Vue関連ツールに多くのコミットをされている@ktsnさんなどが執筆されているかなりの意欲作だと思います。TypeScriptに関する章もあるようです。発売日は9月22日とまだ先ですが、興味があればぜひ予約などしてみてはいかがでしょうか。

Vue.js入門 基礎から実践アプリケーション開発までVue.js入門 基礎から実践アプリケーション開発まで
  • 『Vue.js入門 基礎から実践アプリケーション開発まで』
  • 著者: 川口和也, 手島拓也, 野田陽平, 喜多啓, 片山真也
  • 出版社: 技術評論社
  • 発売日: 2018年9月22日

追記: 2018年12月7日に以下の記事を書きました。TypeScriptをさらに活用する書き方を紹介しています。ぜひ合わせてこちらもお読みください。

関連記事

コメント

    • 必須

    コメント