vue-class-componentをもっと型安全に使いたい話

## ３行で
[Componentに $props を生やすplugin(または型安全なVue Componentのはなし)](http://qiita.com/wonderful_panda/items/36c766c6e89aab2bdf1e) の続きとして、
[vue-typed-component](https://github.com/wonderful-panda/vue-typed-component) というライブラリを作った。
fizz (１行余った)

## これは何？
[vue-class-component](https://github.com/vuejs/vue-class-component) をラップして、`props` や `emit` にTypeScriptのコンパイラチェックが効くようにするためのライブラリ。

## 使い方
`vue-class-component` と同じデコレータ(というか`vue-class-component`のデコレータそのもの)と、コンポーネントの基底クラスとなる `TypedComponent`, `EvTypedComponent` などのクラスを公開している。

使い方は以下のサンプルから察してください。

```ts
import * as tc from "vue-typed-component";

interface ToDoProps { title: string, done: boolean }
interface ToDoEvents { stateChanged: boolean }

@tc.component<ToDoProps, ToDo>({
    // ToDoPropsの定義に基づいてpropsの定義の過不足がコンパイル時にチェックされる
    props: {
        title: { type: String, required: true },
        done: { type: Boolean, default: false }
    },
    template: "<div @click='clicked'><span :style='style'>{{ title }}</span>\\</div>"
})
class ToDo extends tc.EvTypedComponent<ToDoProps, ToDoEvents> {
    // EvTypedComponentを継承することで、$props と $events という２つのインスタンスプロパティが生える
    // $propsだけでいい場合は TypedComponent を使う
    get style() {
        // $props経由で各propにアクセスできる
        if (this.$props.done) {
            return { textDecoration: "line-through" };
        }
        else {
            return {}
        }
    }

    clicked() {
        // $events経由でイベントをemitできる。$eventsには他に $on, $once, $off に対応する on, once, off
        // メソッドもある。
        // いずれも、ToDoEventsの定義に基づいてevent名と引数の型がコンパイル時にチェックされる
        this.$events.emit("stateChanged", !this.$props.done);
    }
}
```

`$data` を持つコンポーネントの場合は `StatefulTypedComponent` とか `StatefulEvTypedComponent` を使う。

```ts
import * as tc from "vue-typed-component"

interface ToDoProps { title: string }
interface ToDoData { done: boolean }

@tc.component<ToDoProps, ToDo>({
    props: {
        title: { type: String, required: true }
    },
    template: "..."
})
class ToDo extends tc.StatefulTypedComponent<ToDoProps, ToDoData> {

    // StatefulTypedComponent/StatefulEvTypedComponentはabstractクラスであり、
    // data() の実装が強制される
    data() {
        return { done: false };
    }

    get style() {
        // dataには $data 経由でアクセスできる。
        // $data はもともとVueコンポーネントが持っているプロパティだが、Stateful～を使う
        // ことでキャストなしでToDoData型として扱えるようになる
        if (this.$data.done) {
            return { textDecoration: "line-through" };
        }
        else {
            return {};
        }
    }

    clicked() {
        this.$data.done = !this.$data.done;
    }
}
```

ついでに Functional Componentの定義も

```ts
import * as tc from "vue-typed-component"

interface ToDoProps { id: string, title: string }

const ToDo = tc.functionalComponent<ToDoProps>(
    /* name */ "ToDo",
    /* props */ {
        // ToDoPropsの定義に基づいてpropsの定義の過不足がコンパイル時にチェックされる
        id: { type: String, required: true },
        title: { type: String, required: true }
    },
    /* render */ (h, context) => {
        // context.props の型がanyではなくToDoProps になる
        const props = context.props;
        return h("span", { attrs: { id: props.id } }, [ props.title ]);
    }
);
```

## できなくなること

`$emit` ではイベントの引数を複数渡すことができるが、`$events.emit` は仕組み上一つしか渡せない。
オブジェクトにまとめれば済む話ではある。

`props` の定義で、 `["title", "done"]` のようにprop名だけの配列を指定することはできない。
何も制約を指定しない場合でも、 `{ title: {}, done: {} }` などのようにする必要がある。

 
## 所感
PropsやEvents, Dataなどをインターフェイスとして定義するのにめんどくささはあるのと、普通ならmixinで片付くところに継承ベースの仕組みを持ち込むことによる窮屈さはあるけど、コンパイラがいろいろチェックしてくれるありがたさには代えがたい。

`Mapped Type` があるなら `vue-class-component` + α で十分なのでは！という気持ち。

## 問題

### `--strictNullChecks` と省略可能なprop

以下のようなコードを例にすると、 `baz` には `required` も `default` も指定されていないので、実行時に `undefined` になる可能性がある。
しかし、インターフェイスとしては `$props.baz` は省略可能になっていないので、`baz` を参照するコードを書いたときに `strictNullChecks` のチェックをすり抜けてしまう。 [^=1]

かと言って、`baz` を `baz?` としてしまうと、`props` の定義のところで `baz` を書き忘れていてもエラーにならなくなってしまうので、それもあまりうれしくない。

```ts
interface Props {
    foo: string,
    bar: string,
    baz: string
}

@tc.component<Props, MyComponent>({
    props: {
        foo: { type: String, required: true },
        bar: { type: String, default: "bar" },
        baz: { type: String }
    },
    template: "..."
}) class MyComponent ...
```

回避策として、`baz?: string` ではなく `baz: string | undefined` とするという手はある。

## TODO
親コンポーネントのrenderメソッド内で、これで作ったコンポーネントをレンダリングする場合もProps, Eventsのインターフェイスが有効に使えるので、そのへんをよろしくやるようなhelperメソッドを追加する。

このへん([keyof で Vuex.Storeを型付けする](http://qiita.com/wonderful_panda/items/752793cd1a8de0150610))の話も合わせてVuexのサポートも統合する。

## 脚注
[^=1]: 実際には、`required` なpropが指定されていない場合でも、コンソールに警告を出力するだけでそのまま動くので、`foo` も実行時に `undefined` になりうる。
  そういう意味で、 `$props` の型を `Props` ではなく `Partial<Props>` にして、すべてのpropに対して `strictNullChecks` が適用されるようにする方が正確ではあるけど、それはそれでうっとうしくないかという気がして迷う。