TypeScriptでどうやるのかわからない

入社してちょうど一ヶ月が過ぎました。人間関係の構築ってどうやるんでしょうか(ぉぃ)。冗談です、仕事に支障が出ないように人間関係を作るのも仕事ですので、その辺はもう慣れてきました。

さて、私の現在の会社(試用期間中)では、なんかアプリコンテストとかいうのをやっています。私自身はアイデアなんてもんは無いのですが、どうやら何かを作らざるを得ないような状態になってしまいました・・・。
決してパブリックなものではなく、社内で閉じているので、じゃあせっかくだから、技術検証みたいなものも兼ねて、TypeScriptで何か作ってみようかなー、ということにしました。

TypeScriptは、あのMicrosoftが発表した、最近とみに需要の高まっている感のある、JavaScript代替言語の一つです。他の言語と違う特徴として、JavaScript(ECMAScript5)のスーパーセットとして言語設計が行われているため、既存のJavaScriptライブラリが(ほぼ)そのまま使える、という利点があります。
詳しくはこちらをご覧ください。開発が進められているリポジトリになります。現在のステータスとしては、「早期プレビュー版」でして、まだ言語仕様が固まりきっていないということで、実際に仕事で使うのはやめておいた方がよろしいかと思います。

TypeScript自体は、かなり力が入った開発らしく、Microsoft謹製のVisual Studioのみではなく、vimemacsにも拡張が公開されていますし、Node.js上で動作させることができるため、事実上どこでも実行できます。また、補完をサポートするライブラリもついているため、Visual Studio以外でも、きちんとした補完をサポートできるようです。

本題

さて、とりあえず試しに、有名なJavaScriptライブラリ enchant.js と、Box2DというライブラリをJavaScriptにポーティングしたもののひとつ、box2dwebを利用してなんか作ってみようとしてみました。

こういう既存JavaScriptライブラリを利用して作成する場合、基本的には.d.tsという「環境宣言」を行うためのファイルに型宣言をしてやって生成してやります。これ自体は、それぞれのドキュメントを見ながら、必要な部分を書いていけばなんとかなります。標準で添付されている、lib.d.tsを見れば、どんな風に書いていくのがいいのかがわかりますが、interfaceで書く以上、staticについてはdeclare varという宣言にせざるをえない、というのがちょっとわかりづらいです。

宣言まわりは頑張ればなんとかなるのですが、私ががっつり嵌ったあげく、解決する手段が見出せなかったのが、「動的に作成されるClassをどう表現するか」ということです。

enchant.jsでは、標準で用意されているクラスを継承したクラスを作る場合などは、enchant.Class.createという関数を利用して、新しくクラスを生成することが基本です。が、この関数、もちろん単純なものではなく、enchant.jsの内部でちゃんと利用できるように、動的に色々と追加したりなんだりとしています。
TypeScriptのclassは、基本的に静的な型情報としてのものですので、動的につっこもうとしても、基本的につっぱねられます。まぁそりゃ当然なんですが。そしてここが一番頭が痛いのですが、TypeScriptにおける継承をJavaScriptに継承すると、「prototypeの範囲」までしか継承されないようなのです。
つまり、prototypeに入っていない、生成された後に動的に追加された要素については無視されます。それが内部で利用するための情報を付加しているような場合だと、よくわからないライブラリ内部のエラーで落ちることになります。(実際落ちました。)

「じゃあinterfaceでサブタイピングすればいいじゃん?」とおっしゃられるでしょうし、それが正しいのでしょうが、例えばenchant.jsのSpriteクラスですが、こいつ自体が他のクラスを継承しているため、合わせて数十にわたるプロパティを持っています。それらをオブジェクトを作成する度に呼びだすとか正気の沙汰に思えませんし、enchant.js側が変更したときの修正範囲が.d.tsで済まなくなります。

じゃあどうすんねん、ということですが、「enchant.jsで継承するクラスについては、newじゃなくてファクトリを使う」みたいな解決?方法があります。言葉で上手く説明できる自身が無いので、次のTypeScriptを見て頂ければ。

    export interface IHoge extends EnchantSprite {
        color: Base.Color;
    }

    export class Hoge {
        private static _singleton : IHoge = null;
        static create(game, width:number,height:number) : IHoge {
            if (_singleton != null) {
                return new this._singleton(width, height);
            }
            var p : any = enchant.Class.create(Sprite, {
                // ここで () => {}をやってしまうと、thisがcreateの時点でのthisになってしまい、正常にいかない。
                // function () {}を使うことで、そのような変換がされなくなる。
                initialize : function () {
                    Sprite.call(this, width, height);
                    this.image = game.assets["hogehoge.png"];
                    this.x = 0;
                    this.y = 0;
                    this.frame = 1;
                    this.scaleX = 1.0;
                    this.scaleY = 1.0
                    this.color = new Base.Color;
                }
            });
            this._singleton = <IHoge>p;
            return new this._singleton(width, height);
        }

    }

    var hoge = Hoge.create(game, 32, 32);
    hoge.z = 0;  // コンパイルエラー!
    hoge.x = 100; // OK!

ものすごい遠回りですが、やっていることとしては、「enchant.jsのenchant.Class.createでclassを拡張する」→「拡張したクラスを、あるインターフェースの実装だとして無理矢理singletonに入れる」→「singletonからnewして返す」という三段構えになっています。
これをやって何が嬉しいかというと、上の通り、
ちゃんと生成したオブジェクトが、IHogeのインターフェースとして扱われる
ということです。というか、こういう型を考慮した開発がしたいからTypeScriptとか他の代替言語を利用したいわけでして、それができなかったらちょっとなー、ということになります、私にとっては。

さて、当然ですが、こうやるとデメリットも色々とあります。

  • わざわざコンストラクタではなく、createを作らないとならない。
  • module内でprivateということができないため、createを入れるためにclassが必要(このへんはなんとかなるのかもしれませんが)
  • せっかく型があるのに、一度anyとして取り扱う必要があり、その時点で一度型情報が失われてしまう。(その間になんかあっても実行時にしかわからない)

まぁ、見様によっては、JavaScript固有の書き方を隠蔽できていて、この関数の外ではちゃんと型情報が使える、ということになっているので、あまり気にする必要が無いかもしれません。

まとめ

今回のはまった問題は、JavaScriptの柔軟すぎる点が仇になった感じです。動的にクラスが生成できるのはいいのですが、どのようにでもオブジェクトを変化させることができるため、静的な型情報をつけようとすると、実行時に無理が出る、という感じでしょうか・・・。すみません、あまりわかってないで言ってますorz。

いずれにしよ、現代的なJavaScriptライブラリは、大なり小なりこのような動的拡張を行っています。が、大抵は環境宣言ファイルだけあればなんとかなります。それは、ほとんどの場合はメソッドやプロパティを使ったり、すでにあるものをnewしたりするだけで済むからです。
enchant.jsで問題が発生したのは、TypeScriptでサポートできない?ような処理が使われていた、ということで、こういう場面では部分的にでもやはりJavaScriptで書く必要がある、ということになります。でも他の代替言語だともっとこみいったことになりそうで、入り乱れないように気をつけさえすれば、限定的に元々のJavaScriptとして使うことも正統なもんだと思います。

なんか問題点ばっかになったようですが、個人的には慣れたJavaっぽいというのもありますし、よっぽどJavaScriptよりは書き易いです。現在のリーダーは、Microsoftだから、ということで嫌がっていましたが、プロダクトの良い悪いは、作ったところに左右されるのではなく、実際のプロダクトを使ってみないとわからないです。
そういう意味でも、今回はいい経験になったと思います。最近OCamlばっか書いていましたので。