TECHNOLOGY 2013.12.24

Flashデベロッパー視点で見たTypeScriptの魅力

  • ActionScript
  • javascript

こんにちは、フロントエンド・エンジニアの荒井です。

今回はJavaScriptを生成する代替言語(いわゆるaltJS)のひとつ、TypeScriptについての紹介です。

普段はFlashコンテンツ/AIRアプリの開発がメインのため、まだまだTypeScriptを案件などで本格導入する程に使いこなしているわけではないのですが、TypeScriptのコードを色々と試してみてこの機能が便利だなとか、こういう使い方をすると良さそうといった内容をFlash開発者の視点を交えて話をしたいと思います。

TypeScriptを選ぶ理由

普段ActionScript3.0(以下、単にActionScript)で開発を行っている私からすると、JavaScriptにはないクラスベースの開発と静的型付けの2つがあればまずは充分なので、HaxeJSXなども候補にはなりました。

ただ、TypeScriptには他のaltJSにはない JavaScriptとの親和性の高さ というメリットがあります。

  • TypeScriptと生成されるJavaScriptのコードの構造が似ていて比較しやすい
  • TypeScript内にJavaScriptのコードが書ける
  • JavaScriptのコードの再編集が容易

個人的にはJavaScriptもある程度理解しながらaltJSを使っていきたかったので、TypeScriptのこの方向性がしっくりきました。

また、これまでJavaScriptを書いていた人でも受け入れやすい言語になっているのではと思っています。
実際、弊社のマークアップチームでも使い始めた人間がいて、 今までJavaScriptを書いていた人とActionScriptを書いていた人を繋げる役割 としても機能するのではないかという期待も込めています。

JavaScriptに変換した例

手始めにTypeScriptで書いた簡単なクラスをJavaScriptに変換してみます。

まずは TypeScript のコードが下記になります。

// クラス
class Point {
    // インスタンス変数
    private _x:number;
    private _y:number;

    // コンストラクタ
    constructor(x:number, y:number) {
        this._x = x;
        this._y = y;
    }

    // インスタンスメソッド
    public distance():number {
        return Math.sqrt(this._x * this._x + this._y * this._y);
    }
}

続いて、JavaScriptに変換したコードが下記になります。

// クラス
var Point = (function () {
    // コンストラクタ
    function Point(x, y) {
        this._x = x;
        this._y = y;
    }
    // インスタンスメソッド
    Point.prototype.distance = function () {
        return Math.sqrt(this._x * this._x + this._y * this._y);
    };
    return Point;
})();

生成されるJavaScriptの構造が元のTypeScriptのコードと似ているので直感的にどう変換されているのかが分かりやすいですね。
TypeScriptを書きながらJavaScriptの勉強もできる所もとても気に入っています。

Playgroundで気軽にコーディング

ちなみに、TypeScript - Playground ではライブコーディングができるので、TypeScriptとJavaScriptを比較しながら気軽にコードを書くことができるので、ちょっとした検証をするのにとても便利なのでオススメです。

先ほどのコードもこんな感じで共有できます。

TypeScriptで気に入った機能

ここから本題に入って、いくつかのお気に入りの機能を紹介したいと思います。

関数型リテラル(関数の引数と戻り値に型付け)

TypeScriptにはActionScript以上に豊富な型付けの方法が用意されています。
その中で便利だなと思った機能のひとつが 「関数型リテラル」 と呼ばれるものです。

class Loader {
    ...
    public loadText(url:String, callback:(text:string) => void) {
        // …
    }
    ...
}

上記のLoaderクラスで定義したloadTextでは、2番目の引数callbackに関数型リテラルを使っていて、 その変数が関数である事に加えて引数と戻り値も指定できる ようになっています。

(text:string) => void

これでひとつの型を表しています。
string 型を引数に持ち、戻り値はなしという意味ですね。

このLoaderクラスを使用したサンプルコードは下記になります。

var loader:Loader = new Loader();

// コールバック関数の引数が一致している
loader.loadText("./xxx.text", callback1);
// コールバック関数の引数が一致しないのでコンパイルエラー
loader.loadText("./xxx.text", callback2);

// 引数がstring型
function callback1(text:string):void {
    // ...
}
// 引数がnumber型
function callback2(text:number):void {
    // ...
}

loadTextメソッドの最初の呼び出しは渡している関数の引数と戻り値の型が一致しているのでコンパイルエラーになりませんが、2回目の呼び出しでは引数の型が一致していないためコンパイルエラーになります。

フロントエンドの開発だと非同期処理を行う事が多いですが、コールバックやイベントリスナーを渡す時の関数の仕様を定義できるので不要なエラーを取り除く事ができて、精神衛生上も良い事だと思っています。

オブジェクト型リテラル(単純なオブジェクトの型付け)

ActionScriptでは、単純なデータをいくつか持つオブジェクトのプロパティに型付けをしたい場合はクラスとして定義する必要がありました。
しかし、 一時的に複数のデータをまとめて保持したいだけなのにわざわざクラスを定義するのも面倒 なので、Objectクラスを使って型付け無しで凌いでしまうケースもあったかと思います。

TypeScriptではこういった用途には 「オブジェクト型リテラル」 を使うと便利だと思います。

// nameとpriceというプロパティを持つオブジェクトを型として指定
var obj: {
    name?:string;
    price?:number;
};
// オブジェクトを初期化
obj = {};
// オブジェクトのプロパティに値を代入
obj.name = "cola";
obj.price = 120;

こういった形でオブジェクトの型を定義しておくと、nameやprice以外のプロパティを使用するとコンパイルエラーが起こりイージーミスを防ぐ事ができます。

ちなみに、型指定のプロパティ名の前に「?」を付けておくと省略可能の意味になります。
これを入れておかないと、オブジェクトの初期化時に必ずプロパティを入れなければならなくなって使い勝手が悪いので、とりあえず入れておくと良いと思います。

// 省略なしだと…
var obj: {
    name:string;
    price:number;
};
// 初期化時にプロパティを入れないとコンパイルエラー
obj = { name="cola", price=120 };

 

interfaceで別名を付けておくと便利

ただ、このままだと型が必要な時に毎回同じ内容を書くのは手間なので、interfaceを使って別名を付けておくと便利だと思います。

// 変数の数だけ型指定を書くのは手間なので…
var obj1: {
    name?:string;
    price?:number;
};
var obj2: {
    name?:string;
    price?:number;
}

// interfaceで別名を付けておけば
interface Item {
    name?:string;
    price?:number;
}
// 使い回しが楽
var item1:Item;
var item2:Item;
var item3:Item;

これで、わざわざクラスを作らなくてもオブジェクト型リテラルとinterfaceを使って単純なオブジェクトを扱えるようになりました。

アロー関数式

TypeScriptの代表的な機能のひとつでもあるアロー関数式ですが、これを使用するとthisの取り扱いがActionScriptの感覚に近くなるので気に入ってます。

class Sample {
    private _message:string = "message";

    // 通常の関数
    public startTimerNormal():void{
        setTimeout(
            function():void {
                // this経由で_messageを参照
                console.log(this._message);
            },
            5000
        );
    }
    // アロー関数式
    public startTimerArrow():void{
        setTimeout(
            ():void => {
                // this経由で_messageを参照
                console.log(this._message);
            },
            5000
        );
    }
}

 
通常の関数とアロー関数式の両方で、いわゆる無名関数を使用しています。
それぞれの中でthisにアクセスしていますが、変換されるJavaScriptのコードに違いが出ます。

var Sample = (function () {
    function Sample() {
        this._message = "message";
    }
    // 通常の関数
    Sample.prototype.startTimerNormal = function () {
        setTimeout(
            function () {
                // ここでのthisはグローバルオブジェクト(window)
                console.log(this._message);
            },
            5000
        );
    };
    // アロー関数式
    Sample.prototype.startTimerArrow = function () {
        var _this = this; // ここでのthisはSampleインスタンス
        setTimeout(
            function () {
                // _this経由なのでSampleインスタンス
                console.log(_this._message);
            },
            5000
        );
    };
    return Sample;
})();

通常の関数では、 JavaScriptの仕様に従って無名関数内のthisはグローバルオブジェクト(window)が参照される のでSample._messageには辿りつけません。
しかし、アロー関数式では TypeScriptで記述していたthisがJavaScriptに変換された時点で_thisとなり、その中に目的のSampleインスタンスが入っているので_messageまでちゃんと辿りつく事ができます。

JavaScriptのthisの挙動を気にせずActionScriptと同じような感覚でコードが掛けるのでとても便利ですね。

 
ただし、アロー関数式を使うとJavaScript本来のthisにアクセスする手段がなくなるため、例えば下記のようなjQueryを使用するケースで問題が出てきます。

class Sample {
    private _message:string = "message";

    public appendMessageEachP():void{
        $('p').each(
            ():void =>{
                // 最初のthisはjQueryが渡してくれるp要素のDOMであって欲しい
                $(this).append("<b>" + this._message + "</b>");
            }
        );
    }
}

アロー関数式でthisが2回でてきますが、最初のthisはjQueryが渡してくれるp要素のDOMを期待したいですが、アロー関数式なので両方ともSampleインスタンスを参照する事になります。

class Sample {
    private _message:string = "message";

    public appendMessageEachP():void{
        // TypeScriptコンパイラに代わって自分で実装
        var _this:Sample = this;
        $('p').each(
            // アロー関数式は使わない
            function(){
                $(this).append("<b>" + _this._message + "</b>");
            }
        );
    }
}

この様な場合は、アロー関数式は使わずに自分で同様の処理をすれば問題ないと思います。

個人的な要望

最後に、こんな機能があったらありがたい!という要望も勝手ながら挙げてみます。

thisの省略

TypeScriptだとクラス内でインスタンス変数/メソッドにアクセスする場合のthisが省略できないのが面倒だなと思っています。
といっても、JavaScriptのスコープとの兼ね合いもあってなかなか実現は難しいのかもしれません。
オプションでも良いので、thisが省略されてたらJavaScriptに変換する際にthisを付けるぐらいのゆるさで実現できたら嬉しい所です。

処理速度の高速化

TypeScriptは素直なJavaScriptを出力するので可読性は高いですが、最適化がされてないので処理速度はJSXやHaxeに比べるとあまり早くないようです。
TypeScriptの本質は処理速度ではないと思いますが、リリースビルドのオプションとかで可読性を無視してでも最適化が行われて処理速度が向上できたら良いなと思っています。

TypeScriptは他のaltJSに比べると後発の技術ですが、これからもアップデートが予定されているので今後にも期待したいですね。
今のバージョンが0.9.5(2013/12/XX現在)なので、次のメジャーアップデートで1.0(正式版)になるのが楽しみでもあります。

それでは、良きTypeScriptライフを!