TypeScript の恩恵を受けつつ Vue を使いたい その1

最近 TypeScript の恩恵を受けつつ Vue を使うためにいろいろと試行錯誤しています。この記事ではコンポーネントの定義と、コンポーネント内のロジックを再利用可能にするための Mixin の定義を TypeScript でどのように書けばよいかを述べます。

vue-class-component を使う

TypeScript で Vue を使う時は vue-class-component はほぼ必須だと思います。なぜなら、Vue のコンポーネントの生成の仕方では、TypeScript コンパイラがコンポーネントの型を解釈することが難しいからです。

例えば、コンポーネントの生成を行うコードは以下のように書けますが、reverseMessage メソッドの中では、this がそのコンポーネントであることがわからないですし、this.message が存在していることもわかりません。

const App = Vue.extend({
  data() {
    return {
      message: 'Hello Vue.js!'
    };
  },
  methods: {
    reverseMessage() {
      this.message = this.message.split('').reverse().join('');
    }
  }
});

const vm = new App({ el: '#app' });

上記のコードを vue-class-component を用いて書き換えると以下のようになります。

import Component from 'vue-class-component';

@Component
class App extends Vue {
  message: string;

  data() {
    return {
      message: 'Hello Vue.js!'
    };
  }

  reverseMessage() {
    this.message = this.message.split('').reverse().join('');
  }
}

const vm = new App({ el: '#app' });

vue-class-component を使用すると、class 構文を使ってコンポーネントを生成できるため、先に述べた、this やそれに生えているプロパティの型がわからないという問題が解消されます。

Vue の Mixin を活用して、責務を分割する

Vue には mixins オプションがあり、あらかじめ定義しておいた機能を一つのコンポーネントに継承する事ができます。これを利用することで、コンポーネントから機能を細かく分割することができます。

例えば、バリデーションのロジックは特定のコンポーネントに持たせるよりは、Mixin にして、再利用可能にするのが良いでしょう。また、同じ機能でも、デザインの都合でコンポーネントを再利用できない場合に、ロジックの部分のみを Mixin にして再利用できるようにするのも良いと思います。

Mixin を作る

Mixin はコンポーネントと同様の書き方で書きます。コンポーネントを作る時と同じように、持たせたいメソッドや、プロパティの定義などを vue-class-component の書き方で書いていきます。

// ユーザー情報の入力値をバリデーションする機能を持たせる Mixin
@Component
class UserValidationMixin extends Vue {
  validateName(name: string) : boolean {
    return name.test(/^[0-9a-zA-Z]+$/);
  }
}

// ユーザー情報の入力を親に通知する機能を持たせる Mixin
@Component({
  props: {
    onChangeName: {
      type: Function,
      required: true
    }
  }
})
class UserUpdateMixin extends Vue {
  onChangeName:  (name: string) => void;
}

Mixin を使うコンポーネントに Mixin の型を持たせる

Mixin を利用するには、コンポーネントの mixins オプションに、Mixin のコンストラクタを渡せば良いです。また、TypeScript に Mixin で定義したプロパティやメソッドを認識させたい時は、Mixin をインタフェースとして渡せば良いです。

以下の例では、先に定義した UserValidationMixinUserUpdateMixin を利用したコンポーネントを定義しています。

@Component({
  // Mixin を適用する
  mixins: [
    UserValidationMixin,
    UserUpdateMixin
  ],

  props: {
    user: {
      type: Object,
      required: true
    }
  },

  template: `
  <form action="#" method="post">
    <input
      type="text"
      name="name"
      :data-error="nameError"
      :value="user.name"
      @input="handleInputName">
  </form>`
})
class UserForm 
    extends Vue
    implements UserValidationMixin, UserUpdateMixin {

  user: IUser;

  validateName: (name: string) => boolean;

  onChangeName: (name: string) => void;

  get nameError() : boolean {
    return this.validateName(this.user.name);
  }

  handleInputName(event: Event) {
    this.onChangeName(event.target.value);
  }
}

Vue v1.0.24 以下での注意点

現在の Vue のバージョン (v1.0.24) では、Vue のコンストラクタを mixins オプションに指定することはできませんが、それを可能にするパッチがマージされたので、次のバージョンでは上の書き方が可能になると思います。

現在のバージョンでこの記事の書き方を実践するには、以下の様な、vue-class-component のラッパーを作ると良いです。

import originalDecorator from 'vue-class-component';

// vue-class-component のデコレータをラップする
function Component(options) {
  if (typeof options === 'function') {
    return originalDecorator(options);
  }
  return function(target) {
    const mixins = options.mixins || [];

    // Vue のコンストラクタは自身が生成された時の options を保持している
    options.mixins = mixins.map(m => m.options);

    return originalDecorator(options)(target);
  };
}

// vue-class-component のように使える
@Component({
  mixins: [MixinA, MixinB]
})
class SomeComponent extends Vue implements MixinA, MixinB {}

つらい部分

この記事のように書くことで、TypeScript で Vue を使うのはかなり良い感じになったのですが、まだまだつらい部分があります。

Mixin の継承先に型を書くこと

Mixin で定義したプロパティやメソッドの型を、継承先のコンポーネントで改めて書かないといけないのがとてもつらいです。TypeScript 自体が Mixin 構文をサポートすればこのつらさもなくなるような気がしますが、TypeScript 公式でこのめんどくさい Mixin のやり方を紹介しているので望みは薄いような気も……。

ただ、ECMAScript の class 式を使えば Mixin っぽいことができるので、この辺の書き方がもっと楽に書けるようになれば良いような気もします。

View の方はまったく型に守られないこと

当たり前なのですが、コンポーネントの template の内部では型が合っているかまったくチェックされません。なので、template の中からメソッドを呼び出すときに、そのメソッドが本当に存在するのか、引数の型が合っているのか、などは人間が注意して書く必要があります。この辺りもちゃんとやりたいなら React とか別のライブラリを使えという話になると思います。

まとめ

TypeScript の利点を受けつつ Vue を使うためには、vue-class-component を使うと良いです。また、コンポーネント内の機能を再利用可能にするために、Vue の Mixin を活用するのも良いでしょう。Mixin を使う時は、mixins オプションに Mixin のコンストラクタを渡すとともに、コンポーネントの宣言時に implements で Mixin を渡すことで、Mixin の型も TypeScript に認識させることができるようになります。

vq にイベントハンドリングの機能を実装した

vq v1.1.1 をリリースしました。主な更新点は、イベントハンドリング機能の追加です。

vq は複雑なアニメーションを簡潔に書くことを目指しているライブラリで、関数型プログラミングの考え方から着想を得ています。今回の更新点以外の詳しい説明は以前書いた記事や README を参照してください。

Example

今回のアップデートで vq.click, vq.focus など、DOM 要素のイベントが発火してからアニメーションを実行するための関数を追加しました。関数の一覧は README に記載しています。例えば、クリックするたびに要素を移動させるようなコードは以下のように書けます。

const box = document.getElementById('box');

// アニメーションのプロパティ & オプション
const move1 = {/* ... */};
const move2 = {/* ... */};
const move3 = {/* ... */};

// box をクリックした時になんらかのアニメーションを実行したい
const clickBox = vq.click(box);

const moveBox = vq.sequence([
  clickBox(vq(box, move1)),
  clickBox(vq(box, move2)),
  clickBox(vq(box, move3)),
  (done) => moveBox(done) // 無限にアニメーションをさせる
]);

moveBox();

また、vq.element という、より低レベルな関数も提供しており、イベントの発火後にアニメーションを実行するかどうかをより細かくコントロールすることができます。第一引数に指定した DOM 要素が、第二引数に指定したイベントを監視します。第三引数には関数を渡すことができ、イベントが発火するたびにこの関数が実行されます。この関数が true を返した時、指定したアニメーションが実行されます。

const alert = document.getElementById('alert');

// アニメーションのプロパティ & オプション
const show = {/* ... */};
const hide = {/* ... */};

// Enter キーが押された時に何らかのアニメーションを実行したい
const pressEnter = vq.element(window, 'keypress', event => {
  return event.which === 13 // press enter
});

vq.sequence([
  vq(alert, show);            // アラートを表示させる
  pressEnter(vq(alert, hide)) // Enter が押されたらアラートを隠す
])();

イベントのハンドリングを vq で扱えるようにしたことで、ユーザーのインタラクションを伴うアニメーションもより簡単にかけるようになったのではないかと思います。それぞれのイベントハンドラはアニメーションとは独立した関数として生成できるため、小さなコードを組み上げるような実装ができ、コードの見通しが良くなることを期待しています。これからの更新では、イベントハンドラの結合や、DOM 以外のイベントを扱うための API を実装する予定です。