TypescriptでVue componentを書くためのライブラリを作った

# TypescriptでVueコンポーネントを書くということ

以下のコンポーネントと同等のものを、typescriptで書くことを考えます。

```javascript
const ToDoItem = Vue.extend({
    name: "ToDoItem",
    template: `
    <div>
        <input type="checkbox" v-model="done">
        <span :style="style">{{ text }}</span>
    </div>`,
    props: {
        title: { type: String, required: true },
        note: { type: String },
    },
    data() {
        return { done: false };
    },
    computed: {
        style() {
            return this.done ? { textDecoration: "line-through" } : {};
        },
        text() {
            return this.title + (this.note ? " - " + this.note : "");
        }
    },
    watch: {
        done: function(value, oldValue) {
            console.log(`check changed: ${this.title}, ${ value }`);
        }
    }
});
```

このコンポーネントは `title`, `note`, `done`, `style`, `text` というプロパティを持つけれども、それはvueが動的に追加するものであってコンパイラがそれを知る術はないので、補完や型チェックも効かずtypescriptを使う嬉しさがありません。

# vue-class-component
[vue-class-component](https://github.com/vuejs/vue-class-component) という準公式ライブラリがあって、これを使うと以下のような形でこの問題を軽減することができます。

```ts
import VueComponent from "vue-class-component";

@VueComponent<ToDoItem>({
    template: `(略)`,
    props: {
        title: { type: String, required: true },
        note: { type: String },
    },
    data() {
        return { done: false };
    },
    watch: {
        done: function(value, oldValue) {　// ※１
            // 以前は、コンパイラからするとここの this は functionのはずなので、this.title などは
            // コンパイルエラーになった
            // 今は、@VueComponentの型引数に指定した型をthisの型だと解釈するようになったので、
            // 普通にプロパティアクセスできるようになった
            console.log(`check changed: ${ this.title }, ${ value }`);
        }
    }
})
class ToDoItem extends Vue {
    //  props, dataのメンバをフィールドとして定義しておく必要がある
    title: string;
    note: string;
    done: boolean;
    // computedに指定していたものはproperty getterとして定義する
    get style() {
        return this.done ? { textDecoration: "line-through" } : {};
    };
    get text() {
        return this.title + (this.note ? " - " + this.note : "");
    };
});
```

これで、`ToDoItem` が `title`, `note`, `done`, `style`, `text` というプロパティを持つことを認識できるようになるので、補完や型チェックが効くようになります。

以前は、デコレータの引数として渡している箇所(※１のところなど)では、`this.title` などのようにプロパティにアクセスすることはできなかったのですが、Vue 2.0 対応と同時に `@VueComponent` に型引数を指定することでそれも普通にできるようになりました。

これで、かなりいい感じにはなるのですが、残る問題として、propsやdataの定義を、デコレータの引数に渡す部分とクラスのメンバとして定義する部分で２回書かなければいけないという話があります。

# vueit
そこのところを解決するために、vue-class-componentを参考にしつつ [vueit](https://github.com/wonderful-panda/vueit) というライブラリを書きました。

vue-class-componentの機能を踏襲しつつ、 `props` の定義についてはフィールドに付加したデコレータから組み立てられるようにしようというのがコンセプトです。

`data` のメンバを２回書かないといけない点については特別なサポートはありませんが、interfaceを定義することによって手間を軽減しつつ
コンパイラに間違いを検出させることができます。
(これはvue-class-componentを使う場合でも有効なテクニックだと思います)

```ts
import { component, prop, watch } from "vueit";

// dataメンバを表すInterfaceを定義する
interface ToDoItemData {
    done: boolean;
}

@component<ToDoItem>({
    template: `(略)`,
    data(): ToDoItemData {      // 戻り値の型を明示的に指定することでコンパイラによるチェックが効く
        return { done: false };
    }
})
class ToDoItem extends Vue {
    // dataのメンバについては、個々にフィールドとして定義する代わりに、$data というフィールドを定義する
    // フィールドが増えてもあんしん
    $data: ToDoItemData;

    // propsのメンバについては、@propで修飾する
    // type validation ({ type: String } の部分)については、typescript上の変数の型をもとに自動で設定する
    // ※type validationの自動設定を有効にするには tscのコンパイルオプションに emitDecoratorMetadata の指定が必要
    @prop.required title: string;
    @prop note: string;

    get style() {
        // dataのメンバには $data 経由でアクセスする
        return this.$data.done ? { textDecoration: "line-through" } : {};
    };
    get text() {
        return this.title + (this.note ? " - " + this.note : "");
    };
    // @watchで修飾したメソッドはwatchに登録される
    @watch("done")
    onDoneChanged(value, oldValue) {
        console.log(`check changed: ${ this.title }, ${ value }`);
    }
});
```

## Why not vue-typescript？

これととてもよく似たコンセプトのライブラリに [vue-typescript](https://github.com/itsFrank/vue-typescript) というものがあります。

実際vue-typescriptを見つけたときはvueitを放棄してこっちに移行しようかと考えたのですが、以下のような理由からvueitに戻ってきました。

**vue-typescriptは Vue 2.x に未対応であること**

これは時間の問題だとは思います。

**data の扱いに不自由な点があること**

vue-typescriptの場合、`data()` も自分で書く必要はなくて、例えば以下のようなコードを書けば `{ done: false }` を返すfunctionを自動的に生成してくれます。この点は非常に楽だし直観的です。

```ts
@VueComponent
class ToDoItem extends Vue {
    done: boolean = false;
}
```

半面、`data()` が返す内容はクラスを定義した時点で固定されてしまうため、例えば以下のように初期値をpropから与えるようなコンポーネントは作成できません。

```ts
Vue.extend({
    props: ["initialValue"],
    data() { return { value: this.initialValue }; }
})
```

vueit は `data()` に関しては何もしないので、通常のコンポーネント定義と同様に手書きする手間はありますが、特別な制限はありません。

**type validationの自動設定**

`props` を定義する時に以下のように型を指定すると、実行時に型チェックをさせることができます。
  
```javascript
props: {
    prop1: String,
    prop2: { type: Boolean, required: true },
    prop3: { type: Number, default: 0 }
}
```

tscのコンパイルオプションに `emitDecoratorMetadata` が含まれている場合、vueitは変数の型に応じてこのtype validationをできるだけ自動で設定します。これは vue-typescript にはない機能です。

```ts
class MyComponent {
    // propの型がstring, boolean, number, functionの場合は、それぞれString, Boolean, Number, Functionを
    // type validationに設定します。
    @prop prop1: string;            // → prop1: String
    @prop.requied prop2: boolean;   // → prop2: { type: Boolean, required: true }
    @prop.default(0) prop3: number; // → prop3: { type: Number, defult: 0 }
    @prop prop4: () => any;         // → prop4: Function

    // 残念ながらunion型は今のところ検出できないので、必要に応じて明示しなければなりません
    @prop({ type: [String, Number]}) prop5: string | number;
}
```


## 小ネタ

### テンプレートのプリコンパイル

Webpackの場合、[vue-template-compiler-loader](https://github.com/fergaldoyle/vue-template-compiler-loader) をインストールして、その出力を `compiledTemplate` に突っ込めばＯＫ

```javascript
/// webpack.conf.js 抜粋
  loaders: {
    { test: /\.html$/, loader: "vue-template-compiler" }
  }
```

```ts
@component({
    compiledTemplate: require("./template.html"),
    /* 略 */
})
class MyComponent extends Vue {
    /* 略 */
}

```

### Functional Component

Vue 2.0 では、dataを持たず、Hookも必要としないコンポーネントについては [Functional Component](http://vuejs.org/guide/render-function#Functional-Components)として定義できるようになりました。

```javascript
const ToDoItem = Vue.extends({
    name: "ToDoItem",
    functional: true,
    props: ["text", "done"],
    render(h, context) {
        const { text, done } = context.props;
        const style = { textDecoration: done ? "line-through" : "none" };
        return h("div", { style }, [ text ]);
    }
})
```

これを、以下のように書けるようにしてみました。 `render()` の３番目以降の引数がpropsになります。

```ts
@functionalComponent
class ToDoItem extends Vue {
    render(h, context, text: string, done: boolean) {
        const style = { textDecoration: done ? "line-through" : "none" };
        return h("div", { style }, [ text ]);
    }
}
```

`@prop` でpropsのオプションを指定することも。

```ts
@functionalComponent
class ToDoItem extends Vue {
    render(h, context, 
           @prop.required text: string,
           @prop.default(false) done: boolean) {
        const style = { textDecoration: done ? "line-through" : "none" };
        return h("div", { style }, [ text ]);
    }
}
```

と言いつつ、インターフェイスに迷っているところがあるのでまだmasterにはマージしていませんが。

### props を interface として定義する話

なにを使うにしろ使わないにしろ、propsをinterfaceとして定義しておきたくなることが多いんじゃないかと思います。

```ts
import { component, prop, watch } from "vueit";

// ToDoItemコンポーネントが持つpropsをinterfaceとして定義する
interface ToDoItemProps {
    title: string;
    note?: string;
}

@component<ToDoItem>({ ... })
class ToDoItem extends Vue implements ToDoItemProps {
    // ToDoItemPropsをimplementすれば、ここでフィールド名などを間違えていないことが ** 部分的に ** 保障される
    // 「部分的に」というのは、省略可能としてinterfaceに定義したメンバの方は間違えても検出されないから
    @prop.required title: string;
    @prop note: string;

    get style() { ... };
    get text() { ... };
});
```

例えば、このinterfaceを使って下のようなメソッドを用意しておけば、

```ts
function renderToDoItem(h, props: ToDoItemProps, options: Vue.VNodeData): Vue.VNode {
    return h(ToDoItem, Object.assign({ props }, options));
}
```

親のrenderメソッドでToDoItemを出力するときに渡すpropsをコンパイラにチェックさせることができます。

```ts
function render(h, options: Vue.VNodeData): Vue.VNode {
    return h("div", { staticClass: "todo-item" }, [
        renderToDoItem(h, { text: "焼肉を焼きに行く" })
    ])
    return h(ToDoItem, Object.assign({ props }, options));
}
```

ただ、そうすると結局propsの内容を２か所に書くことになるので、どうしたもんかなあという感じ。
それなら vue-class-componentでもいいんじゃないのという気もするし・・・

# 雑なまとめ

* Typescriptでvueのコンポーネントを書くライブラリはたくさんあるけど、微妙にほしいものと違う気がするので自分で作った

* Vue in Typescriptのベストプラクティス的なものについてはまだまだ考え中

* 意見とかもらえると喜びます