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を利用してはいかがでしょうか。