GenericsでBackbone.jsのModelっぽいものを作ってみる

TypeScriptが、0.9.0.alphaとして、preview releaseが行われています。http://typescript.codeplex.com/releases/view/105503

0.9.0の目玉機能は、何といってもGenericsとOverload on constantsです。これが加わることで、大体Java5くらいの表現力があるんじゃないかと思います。
Genricsが使えるということは、型による制御が色んなところでできるんじゃね?という思いと共に、最近トレンドになってきたクライアントサイドMVCとかにもこれが生かせるんじゃないかとも思います。
ということで、これが使えて(多分)一番嬉しいと思われる、ModelをGenericsでなんとか表現できないか、色々試してみました。

この記事は、TypeScriptのrelease-0.9.0.alphaブランチを利用しています。バグが残っているかもしれませんし、そもそも細かい仕様とかが変わるかもしれませんので、その辺は最新の情報を参照してください

TypeScriptのGenerics

その前に、TypeScriptでのGenericsはどんな感じのものなのか見てみます。TypeScriptの仕様書に十分なサンプルが載っていますので、そこから引用すると

interface A { a: string; }

interface B extends A { b: string; }

interface C extends B { c: string; }

interface G<T, U extends B> {
    x: T;
    y: U;
}
var v1: G<A, C>; // Ok
var v2: G<{ a: string }, C>; // Ok, equivalent to G<A, C>
var v3: G<A, A>; // Error, A not valid argument for U
var v4: G<G<A, B>, C>; // Ok
var v5: G<any, any>; // Ok
var v6: G<any>; // Error, wrong number of arguments
var v7: G; // Ok, equivalent to G<any, any>

Java5とかを御存じの方なら、これで大体把握されると思います。ちなみに、TypeScriptにおけるinterface/classは、基本的に全てJavaScriptのObjectリテラルで書くことができるので、上のGクラスでは、

{x:0, y:""}

とかを渡してもちゃんと動作します。

Genericsの制限

TypeScriptでは、型(特にinterface)は、JavaScriptの出力時に基本的に削除されます。型情報は、TypeScriptのコンパイル時における型チェックに利用されるためのもの(らしい)です。
そういうことなので、型アノテーションの部分の名前空間と、式の部分の名前空間は分断されている状態になっています。Javaとかでも出来なかったと思いますが、

class Hoge<T> extends T {}

みたいなことはできません。型パラメータの部分は特定のclassではありませんが、extends Tの部分は、特定のclassでなければならないからです。

同様に、上の例で言うところのTについて、型アノテーションの部分では利用できますが、式(普通のプログラム)では利用できません。書いてみても、「んな型知らん」と怒られます。怒られてもJavaScriptが出力されることはされますが。

この制限は、TypeScriptではJavaのようなことはできない、ということがわかります。JavaはClassクラスがあれば色々とわかりますが、TypeScriptではそもそもそんなものはないので、型アノテーション以外で型の情報を利用することができません(多分)。

Backbone.jsみたいなModelを作ってみる

さて、ここまでを踏まえたところで、Backbone.jsみたいなModelを作ってみます。書いてる本人はBackbone.jsをよく知らないので、情報が古いと思いますが、こちらを参考にして作ってみました。
無論、検証版なので、機能はがっつりと削っていますし、バグについては何をいわんや、という状態です。こんなこともできるんだ、という参考程度でお願いします。

interface ModelOption<A> {
    initialize?() :void;
    defaults() : A;
}

class Model<A> {
    private _option :ModelOption<A>;
    constructor(option:ModelOption<A>) {
        if (option) {
            this._option = option;
        }
    }
    _(a?:A) : A {
        var _a = {};
        if (a) {
            Object.getOwnPropertyNames(a).forEach((v) => {
                Object.defineProperty(
                    _a, v,
                    {get: () => {return a[v];},
                     set: (val) => {a[v] = val;}
                    });
            });
        } else {
            var d = this._option.defaults();
            Object.getOwnPropertyNames(d).forEach((v) => {
                Object.defineProperty(
                    _a, v,
                    {get: () => {return d[v];},
                     set: (val) => {d[v] = val;}
                    });
            });
        }
        return _a;
    }
}

class A {
    constructor(public hoge:number) {}
}

var m = new Model<A>({defaults : () => {return {hoge:1};}});

var h = m._();
console.log(h.hoge);  // Ok
console.log(h.huga);  // Compile Error!!

さて、一応こんな感じで作成できます作成したmodelで、ベースとなる型に存在しないプロパティを指定すると、ちゃんとコンパイルエラーになってくれるので、本家のようにget/setで扱う必要がありません。もっとBackbone.jsのModelに似せることもできますが、それだとJavaScriptをそのまま書くのと変わりませんので。

もちろん欠点もあって、このやりかたは、参考にしたBackbone.jsよりも直感的ではありませんし、記述が多少面倒ではあります。しかし、汎用のgetter/setterを利用する必要が基本的に無いということで、Modelの実体を扱う、という側では、こっちの方がわかりやすいような気がします。

Genericsを使ってみて

TypeScriptの0.9では、ArrayもGenericsが使えるようになっており、それだけでもかなり有用だとは思いますが、型の情報を利用できない、というのが結構ネックになってしまいました。JavaScriptなので、Objectの情報を知らべればいいじゃん、ということなんでしょうが、undefinedだったりnullだったりというのからできれば開放されたくてGenericsを利用する、という部分もあると思うので、なんとか型の情報を利用できるようにしてもらえんかなー、とも思います。

しかし、Genericsが導入されたことで、TypeScriptはよりよくなると思います。JavaScriptのフリーダムっぷりに嫌気が差したけれども、CoffeeScriptとかを学ぶほどでもないなー、という方は、基本的にJavaScriptコンパチなTypeScriptを利用してはいかがでしょうか。