「静的型付けが出来るJavaScriptの上位言語」を触ってみよう。パート2 - TypeScript #2 (TypeScript 0.9の新機能)

えぇとですね。

前回終わりにこんな事を書いてました

次回「TypeScript後半戦」として以下のようなことを試してみます。
Any
thisの解決
...

が、しかし。

マイクロソフト、「TypeScript 0.9」をリリース

いやぁ〜絶妙なタイミング!!!

前回の告知を大いに裏切り(すいません...)「TypeScript 0.9」を早速触ってみようと思います。

「TypeScript 0.9」新機能

折角なんで「TypeScript 0.9」から実装された新機能を試してみます。

今回はTypeScript 0.9 公式アナウンスを参考にします。

Generics

ジェネリクスですねぇ。 総称型。実装時に型指定するあれですねぇ。

ジェネリクスと言えばリスト要素型の指定

var array: Array<string> = ["John", "Sam", "George"];
  • Array<string>で配列の要素型を指定できる
  • 要素の型が保証されるのでarray[0].lengthのように要素を直接string値として使用可能
  • 以下のように指定した型以外の型の要素を入れようとするとコンパイルエラー
var array1: Array<string> = ["John", "Sam", "George", 1];

var array2: Array<string> = [];
array2.push(1);

要素型が指定出来ないと配列はどんなオブジェクトでも入るが、 出した後なんのオブジェクトかわからないから結局型チェック的なことが必要になったり。 これで入れる側・使う側双方スッキリですね。

ジェネリックメソッド

ジェネリックメソッドも使える

interface Array<T> {    // 1.
    // ...
    map<U>(callbackfn: (value: T, index: number, array: T[]) => U): U[];    // 2.
    // ...
}

var array: Array<string> = ["John", "Sam", "George"];   // 3.

var lengths = array.map((val, idx, arr) => val.length); // 4.

サンプルコードのまんまですね。一応解説しておくと

  1. ↑で書いたように要素型をジェネリクスで定義できるArrayインターフェースがある
  2. Arrayにはmapメソッドがある
    1. コールバック関数を引数で受け取る
    2. 全ての要素に対して引数のコールバックを実行
      • コールバックの第1、第3引数は「1.」で指定された型が反映される
    3. 実行結果を配列にして返却
      • コールバックの戻り値型が返却する配列の要素型に反映される

    (「1.」「2.」はこんなAPIが実装されていると言う表現なので、実際には定義不要)

  3. 要素型stringで配列を生成
  4. mapメソッドを実行
    • Array<string>でしていした要素型が反映され為、コールバック引数:valはstring型であることが保証される
    • valはstring型なのでコールバック関数内でval.lengthは実行可能
    • val.lengthはnumber型なのでmapメソッドの戻り値は要素型:numberな配列となる

独自クラスのジェネリクス

class Sample<T> {   // 1.
    val: T;
}

var sample: Sample<string> = new Sample<string>();  // 2.
sample.val = 'sample';          // 3.
var val: string = sample.val;   // 4.

class SubSample extends Sample<string> {    // 5.

}
  1. ジェネリッククラス定義
  2. string型指定でインスタンス生成
  3. sample.valの初期化値はstring型以外だとコンパイルエラー
  4. sample.valは必ずstring型となることが保証される
  5. 継承時のジェネリクス指定

その他

Javaにあるような以下の定義は使えない模様

Overloading on Constants

オーバーロードはこんな感じで定義

function func(val: string): string;
function func(val: string, num: number): string;
function func(val: string, num?: number): string {
    if(num) return val + num;   // 引数numの存在チェック
    return val;
};
// function func(num: number): string;  // ここにオーバーロードメソッドを定義するとコンパイルエラー
  • 関数本体は1箇所に定義して引数パターンをチェック

オーバーロードはv0.8以前にもあった機能ですが、今回のバージョンアップで関数本体以降にオーバーロードが定義できなくなったらしいです。

関数本体が一箇所にしか定義できないのはぐぬぬな感じなのでこのくらいで。

Export =

なんか若干間違った解釈のような気もするが、別ファイルのクラスをファイル指定でクラスとして読み込む機能のようである

client.ts(読み込み元ファイル)

// export対象クラス
class Client {
    constructor(public name: string, public description: string) { }
}

export = Client;    // クラスをexport

app.ts(読み込み先ファイル)

import MyClient = require('./client');  // ファイル名指定で``require``(読み込み)
var myClient = new MyClient("Joe Smith", "My #1 client");
  • exportしたクラスはそのファイルの代表クラス(public class)のようなもの
  • exportは2つ以上定義するとコンパイルエラー
  • require指定する際、拡張子は不要(付けるとコンパイルエラー)
  • importで定義するクラス名は読み込み元クラス名と合わせる必要はない(同名、別名どちらもOK)
  • 読み込み先tsをコンパイルすると読み込み元tsもコンパイルされる

で、これをコンパイルしてみると、exportimportはこんな感じになる

// export
module.exports = Client;

// import
var MyClient = require('./client');

はい。このmodule.exportsrequireはなんなんだと。

これはnode.js等のサーバサイドJavaScriptで実行することを想定した定義です。 ブラウザ上では動作しません

  • node.jsのrequire,module.exportsはこちらで詳しく解説されています。

Enums

列挙型。ある程度のボリュームのJavaScriptを書くと絶対ほしくなる。

サンプルはこんな感じ

enum Color { red, blue, green }

var myColor = Color.red;
console.log(Color[myColor]);

これをコンパイルすると

var Color;
(function (Color) {
    Color[Color["red"] = 0] = "red";
    Color[Color["blue"] = 1] = "blue";
    Color[Color["green"] = 2] = "green";
})(Color || (Color = {}));

var myColor = Color.red;
console.log(Color[myColor]);

コンパイルすると0からのインデックス値がそれぞれのEnum要素に設定されると

じゃ数値との比較はできるのか??

if (myColor === 0) return;

↑のコードはOKです。コンパイルエラーになりません。

じゃあじゃあ列挙体の各要素値を自分で指定できる??

enum Color { red = 10, blue = 20, green = 30 }

これもOKです。ちゃんとコンパイル後のJavaScriptに値が反映されます。

  • 値はnumber型のみ指定可能(文字列等は指定できない)
  • 値の重複はコンパイルエラーにはならない

Declaration Merging

定義済class、functionなどのデフォルト値を別の定義から設定出来る模様

function greet(lang = greet.lang) { // 1.
    console.log('hello ' + lang);
}

module greet {  // 2.
    export var lang = "java";   // 3.
}

greet('typescript');    // 引数を設定した場合の実行
greet();                // 引数を設定しない場合の実行
  1. 同名モジュールgreetlangプロパティを引数のデフォルト値に設定
  2. 別の定義でmoduleを定義
  3. lang値を定義してmoduleへ追加

これを実行してみると

$ node merging.js
hello typescript
hello java

期待通り

  • 引数を指定した場合: greet引数は指定値(typescript)を使用
  • 引数を指定しない場合: greet引数はmodule greetで指定したデフォルト値langjava)を使用

もちろんfunction内で引数判定して切り替えることも可能だが、これは別モジュールとして定義出来ることにメリットがある

  • ↑の場合、module greetを定義しなくてもコンパイルエラーとはならない模様

ちなみにこれのコンパイル結果は以下

function greet(lang) {
    if (typeof lang === "undefined") { lang = greet.lang; }
    console.log('hello ' + lang);
}

var greet;
(function (greet) {
    greet.lang = "java";
})(greet || (greet = {}));

greet('typescript');
greet();

以上、ざっと新機能紹介してみました。

さらっと触れる程度しか紹介できてませんでしたが、型に厳密な言語では必須になっているGenericsやEnumが実装されたことで、かなり実用的になった感が増したのは私だけでしょうか。

また、TypeScriptを使用するメリットの一つとして、必ず型を定義する必要はないと言うことが挙げられます。

厳密に型を定義するところと、柔軟にサクサク実装するところを上手く使い分けることで、よりTypeScriptの魅力が引き立つのではないかと考えています。

次回

今度こそほんまに「TypeScript最終回」です。以下のようなことを試してみます。

  • Any
  • thisの解決
  • module(namespace)
    • export
    • import
    • ファイルインポート
  • *.d.ts
    • jsファイルからのコンパイル
  • 外部ライブラリの使用
  • IDE
本日参考にさせていただいたサイト

ありがとうございました。