TypeScript入門―大規模開発に適したJavaScript互換言語

AltJSとTypeScriptとは

昨今、高度なWebアプリケーションやNode.jsによるサーバサイドアプリケーションなど、JavaScriptによるアプリケーションの開発が多く見られるようになってきました。それに伴い、アプリケーション開発の効率化のためのAltJSと呼ばれる言語(コンパイルするとJavaScriptを書き出す非JavaScript言語)たちが注目され、開発が盛んに行われています。TypeScriptはAltJSの1つです。

TypeScript以外にもさまざまなAltJSがあります。たとえばLLLightweight Languageを意識した書きやすいJavaScriptとしてのCoffeeScript[1]⁠、国産で速度最適化を重視しているJSXJavaScript以外の言語への変換ができるHaxeなどです。TypeScriptは、よりよいJavaScriptというだけでなく大規模開発に向いているという特徴を持ち、それなりに複雑なプロジェクトで利用するケースなどで支持を集めています。静的な型付けがあること、素のJavaScriptやC#、Javaなどに似た構文なども特徴です。

TypeScriptの開発はC#やTurbo Pascalの開発者として有名なAnders Hejlsberg氏が中心となってMicrosoftで開発されました。

開発の中心はMicrosoftですが、コンパイラはNode.js上で動作するオープンソース(Apache License 2.0)として公開されています。フィードバックも受け入れる開かれたコミュニティになっていることも支持を集めているポイントでしょうか。

TypeScriptを今すぐに試してみる

TypeScriptのインストールについてはのちほど解説しますが、そもそもインストールするのは面倒だ、雰囲気だけ味わってみたいという方も多いと思います。そのような方のために、TypeScriptの公式サイトにはTypeScriptを書けば即座にJavaScriptに変換/実行できるPlaygroundと呼ばれるページが用意されています。このあとのコードサンプルもPlaygroundで変換の確認ができるので、ぜひ動かしてみてください。

TypeScriptの特徴とメリット

TypeScriptが実際にどのような言語なのか、なぜ大規模な開発に向いているのか、その特徴やメリットを見ていきましょう。

TypeScriptのおもな特徴は次のとおりです。

  • 静的型付け言語であり、あいまいさを回避しやすい
  • ECMAScriptのスーパーセットで、かつECMA Script 6の機能を先取りしている
  • 変換しても平易なECMAScriptが出力される
  • 既存ライブラリとの相互運用のために外部に型定義ファイルを持てる

TypeScriptの文法

TypeScriptの文法は基本的にJavaScriptの言語部分(ブラウザのDOMなどの仕様を含まない)であるECMAScriptのスーパーセットになっています。つまり、JavaScriptのコードはそのままTypeScriptのコードになります。TypeScriptのコード例を次に示します。

function sayHello(message) {
    alert(message);
}
sayHello("Konnichiwa!");

このTypeScript のコードを変換すると次のJavaScriptのコードになります。

function sayHello(message) {
    alert(message);
}
sayHello("Konnichiwa!");

この2つのコードを比べるとわかりますが、変換の前後に違いはありません。このようにTypeScriptはECMAScriptをベースとしているため、比較的馴染みやすい文法となっています。裏を返せばJavaScriptという言語由来の部分も多くあるため、JavaScriptの知識をある程度必要とします。TypeScriptの文法の一部は現在策定中のECMAScript 6の機能を先取りして実装していますが、それらはECMAScript 5(または4)相当の文法に変換されます。

先ほどの変換例のとおり、TypeScriptのコンパイラは極力平易で人間が読みやすいECMAScriptを出力しようとします。

ではJavaScriptのコードをTypeScriptのコードにコピー&ペーストすればそのまま使えるかというと、そうとは限りません。先ほど特徴の1つとして挙げたように、TypeScriptは静的型付け言語なので、ライブラリやブラウザの機能を呼び出している部分のチェックで文法エラーになることがほとんどです。

TypeScriptは静的型付け言語

TypeScriptの特徴は、なんと言ってもその名にTypeとあるとおり、静的型付け言語であることです。WebでJavaScriptを書くことが主であった方々には、静的な型はあまり馴染みがないかもしれません。

静的型付け言語とは、あらかじめ変数などに型、つまり文字列であればstring数値であればnumberなどを宣言しておき、コンパイル時に型の整合性をチェックする言語です。実際にどうなるのか、先ほどのサンプルコードに型の宣言を追加した例を示します。

function sayHello(message : string) {
    alert(message);
}
sayHello("Konnichiwa!");

この例では、sayHello関数は引数として文字列しか受け取れないことを示すため、: stringという宣言を追加しています。たとえば最後のsayHello関数の呼び出しで引数に数値の1を指定してコンパイルすると「引数としてstring型を受け取るはずだけどnumber型が渡されていて、それを受け取るものはなかったよ」というエラーが発生します。変数の型も同様に宣言できて、意図しない値の格納を防げます。

var message: string; // string(文字列型)の変数を定義
message = 1;
// ↑ エラー: Cannot convert 'number' to 'string'.

このように実行前のチェックが可能になるのが静的型付け言語のメリットです。静的な型付けをするメリットはのちほどもう少し詳しく紹介します。なお、Playgroundなどで確認するとわかりますが、このコードの変換結果は型宣言をしなかった場合と同じになります。つまり、型情報はコンパイル時にのみ使われ、コンパイル後のコードには情報として残りません。

大規模開発をサポートする要素

TypeScriptには、大規模で複雑化するアプリケーションの開発を行いやすくする要素が含まれています。その1つは静的な型付けですが、その他にはクラス定義や名前空間といったモジュールの要素があります。C#やJava、Rubyなどの言語に触れたことがある方にとっては馴染み深いのではないでしょうかリスト1⁠。

リスト1 モジュールの例
module Sample {
    export class Greeter {
        message: string;
        constructor(message: string) {
            this.message = message;
        }
        sayHelloAfterSecond(): void {
            setTimeout(() => alert(this.message), 1000);
        }
    }
}
new Sample.Greeter('Konnichiwa!!').
sayHelloAfterSecond();

モジュールを利用すると、素のJavaScriptを書いていたときには開発者によってまちまちだったクラスの定義方法やモジュール分割が統一でき、開発者間での共有や読みやすさの向上が期待できます。プロジェクトの規模が拡大することがわかっている場合は大きなメリットになるでしょう。

静的型付け言語であるメリット

TypeScriptが静的型付け言語であり、それが大きな特徴であることは繰り返し書いてきました。しかし、静的型付け言語に馴染みのない方も多いと思いますので、どのようなメリットがあるのかを具体的に少し掘り下げて解説します。

コンパイル時のエラー

先ほど述べたとおり、関数が文字列を期待しているのに数値を渡している場合や、対象のクラスには存在しないメソッドを呼び出している場合など、コードに明らかなエラーがある場合はコンパイル時にエラーが発生します図1⁠。つまり、名前の間違えや想定外の操作を実行前に修正できます。

図1 TypeScriptのコンパイルを実行した際にエラーが出た例
図1 TypeScriptのコンパイルを実行した際にエラーが出た例

これはリファクタリングなどを行う場合にもメリットになります。たとえば特定のクラスのメソッド名を変更した場合、利用している個所の修正漏れがあればコンパイル時にエラーが発生するので、漏れを見逃しにくくなります。コードの規模が大きくなればなるほど、その恩恵を受けられるでしょう。

Visual StudioやWebStormなどのIDEとの相性の良さ

Visual StudioやWebStormといったいわゆるIDEIntegrated Development Environmentのエディタを利用すれば、コーディング時にサポートを手厚く受けられるというのもTypeScriptの大きなメリットの1つです。特に開発元が同じMicrosoftということもあり、Visual Studioのサポートはとても手厚いです。

IDEのサポートで代表的なものと言えばコード補完ではないでしょうか。Visual StudioやWebStormでも、コード補完機能でメソッド名やプロパティ名が補完されます。これによりタイピングやリファレンスを探す手間が省けます図2⁠。

図2 エディタ上でのコード補完の例
図2 エディタ上でのコード補完の例

またコードのエラーをエディタ上に表示する機能もあります。常にコードの状態を確認できるので、コンパイル前にミスに気づくことができます図3⁠。

図3 WebStormで文法エラーのチェックが働いている様子
図3 WebStormで文法エラーのチェックが働いている様子

IDEらしい強力な機能としては、リファクタリングサポートもあります。たとえばメソッド名を変更するとき、1ヵ所名前を変更すればそのメソッドを参照している名前をすべて書き換えてくれる機能があります図4⁠。

図4 Visual Studioでメソッド名のリネームを実行している様子
図4 Visual Studioでメソッド名のリネームを実行している様子

このような機能は静的な型付けをしているおかげで正確に動作できているのです。Visual StudioやWebStormにはJavaScriptのコード補完のサポートもありますが、それらはIDEが推測した結果やコードを疑似実行した結果を用いているため、見落としも多くあります。静的型付け言語ではモジュールとメソッドの関係などをエディタが正確に把握できるので、サポートも正確です。

ハイライトやリネームの機能はPlaygroundでも実際に体験できますので、ぜひ試してみてください。

TypeScriptのインストールとコンパイル

Playgroundでコードを書くのは即座に試せてよいのですが、実際に開発でTypeScriptを使う場合はファイルにコードを書いてコンパイルするという流れになります。ここでは、コンパイルのためのツールのインストール方法とコードのコンパイル方法を簡単に紹介します。

インストールする

Windowsをお使いの方はVisual Studioを利用するのが簡単でしょう。Visual Studio 2013のバージョンUpdate 2以降にはTypeScriptの開発ツール(エディタとコンパイラ)が含まれるので、インストールすればすぐにコードを書けます。Visual Studio 2013は有償と思われがちですが、無償で利用できるVisualStudio Express 2013 for Webもありますのでこちらの利用をお勧めします図5⁠。

図5 Visual Studioではコード補完やシンタックスハイライトなどが利用可能
図5 Visual Studioではコード補完やシンタックスハイライトなどが利用可能

またOS XやLinux、WindowsでVisual Studio以外のエディタを利用している方は、Node.jsのパッケージとしてTypeScriptのコンパイラをインストールできます。管理者権限を持ったコンソールで、またはsudoを付けて次のコマンドを実行してください。

% npm install -g typescript

このコマンドを実行してインストールが完了すると、TypeScriptコンパイラであるtscコマンドを実行できるようになります。

コードを書いてコンパイルする

Visual Studioでは、プロジェクトを新規作成するときにTypeScriptのテンプレートを選択するとTypeScript用のプロジェクトが作成され、TypeScriptのファイルの編集が可能になります。そしてTypeScriptのファイルを編集すると、自動的にコンパイルが行われてJavaScriptファイルが生成されます。

Node.jsのパッケージとしてtscコマンドをインストールした場合は、お好みのテキストエディタでTypeScriptのファイル(拡張子は.tsを作成し、そのファイル名をtscコマンドの引数として渡せばコンパイルされます。

% tsc script.ts

tscコマンドを実行すると、引数に指定した.tsファイルと同名の.jsファイルが生成されます。しかし、開発中に何度も書き換える場合、編集するたびにtscコマンドを実行するのは面倒です。そのため、tscコマンドには変更を監視して自動でコンパイルする--watchオプションが用意されています。

% tsc --watch script.ts

--watchオプションを指定してtscコマンドを実行すると終了せずにそのまま待機状態になり、ファイルを編集すると自動的にコンパイルされます。

TypeScriptの文法

JavaScriptにも型がありますが、TypeScriptは静的型付け言語ですから、より型を意識したコーディングになります。

型と変数の宣言

TypeScriptは組み込みで次の型を持っています。

  • string:文字列型
  • number:数値型
  • boolean:ブール値(true/false)
  • void:何もない、空からを表す型

ほかにも関数やオブジェクト、nullなどの型もありますが、よく使うのは上記の4つでしょう。

TypeScriptでの変数宣言はJavaScriptと同様varを使います。宣言の変数名の後ろにコロンと型名を書くと変数の型を指定できます。

var text: string;
var count: number;
var enabled: boolean;

変数の型が指定されている場合、その変数に指定した型以外の値を代入しようとするとコンパイルエラーになります。

// 文字列型の変数に数値を代入しようとしてエラー
text = 1;
// 数値型の変数に文字列を代入しようとしてエラー
count = "1";
// ブール型の変数に数値を代入しようとしてエラー
enabled = 0;

しかし、変数を宣言するたびに型を指定するのは面倒です。TypeScriptでは変数宣言で初期値を代入する場合は型の指定を省略できます。たとえば次のようなコードになります。

var text = "hauhau";
var count = 10;
var enabled = true;

一見JavaScriptと同様に見えますね。TypeScriptでは型推論というしくみにより、このコードの変数は暗黙に型が指定された扱いになります。つまり、たとえば初期値として文字列が代入されている場合、コンパイラは「最初に文字列を代入しているということは、この変数の型は文字列型だね」と判断して、その変数を文字列型として扱うのです。当然、型の合わない変数に値を代入しようとするとコンパイルエラーが発生します。

// 文字列型の変数として初期値を持たせつつ定義
var text = "hauhau";
// 文字列型の変数に数値を代入しようとしてエラー
text = 1;

これでまったく異なる型の値を間違えて代入することや混在することがなくなります。気軽に記述しながら静的型付け言語の恩恵を受けられるのです。

関数の定義

TypeScriptでの関数の定義は、基本的にはJavaScriptと同じですリスト2⁠。functionキーワードに続けて関数名を書き、引数を取ってブロックを続けるという形です。JavaScriptとの違いは、引数と関数の戻り値の型を指定できるところです。関数がどのようなデータを受け取ってどのようなデータを返すのかを厳密に指定できます。引数の型指定は変数と同様、引数名の後ろにコロンと型名を書きます。戻り値の型指定は引数リストの後ろにコロンと型名を書きます。

リスト2 関数定義の例
// 引数aとbはnumber型で、戻り値がstring型
function addAndToString(a: number, b: number): string {
    return (a + b).toString();
}
addAndToString('1', '2'); // エラー: パラメータはnumber型しか受け取らない
var value: number = addAndToString(1, 2); // エラー: number型の型にstring型は入ない

JavaScriptには通常の関数定義のほかに匿名関数(無名関数)があります。たとえば次のコードのように、イベントハンドラとして登録するときなどによく利用されます。

document.body.addEventListener('click', function () {
    alert('clicked!');
});

TypeScriptでは、このような無名関数をArrow FunctionというECMAScript 6を先取りした記法で記述できます。Arrow Functionはその名のとおり矢印を使います。先ほどのコードをArrow Functionで書き換えると次のようになります。

document.body.addEventListener('click', () => {
    alert('clicked!');
});

長かったfunctionキーワードがなくなり、代わりに=>が登場しました。functionがなくなったおかげでスッキリしています。

さらに、このArrow Functionにはブロックを取る書き方と取らない書き方の2種類があります。次の2行のコードは、書き方は異なりますが同じ処理です。

[1,2,3].map((v) => { return v * 2; });
[1,2,3].map((v) => v * 2);

ブロックを取る書き方はfunctionを使う定義とほとんど変わりませんが、ブロックを取らない書き方は戻り値を直接書くのでさらにスッキリします。このコードの変換結果を見ると、どちらも同じJavaScriptのコードに変換され、TypeScriptではスッキリ書けていたことがわかります。

[1, 2, 3].map(function (v) {
    return v * 2;
});

なお、Arrow Functionの引数が1つだけのときは括弧を省略できて次のようにも書けます。

[1,2,3].map(v => { return v * 2; });
[1,2,3].map(v => v * 2);

クラスの定義

TypeScriptでクラスを定義するには、ECMAScript 6から先取りして導入されたclassを使います。その際にprototypeベースの開発知識を必要としないのがよいところです。実際にクラスを定義するコードの例を次に示します。

// Greeterクラスを定義
class Greeter {
    // string型のプロパティ
    message: string;
    // パラメータを取るコンストラクタ
    constructor(message: string) {
        this.message = message;
    }
    // メソッド
    sayHello(): void {
        alert('Hello! ' + this.message);
    }
}
new Greeter('Konnichiwa').sayHello();

クラスの定義にはプロパティ、メソッド、コンストラクタを含めることができます。メソッドの定義は関数の定義とほぼ同じ形ですが、functionキーワードは使いません。

定義したクラスを使うにはJavaScriptとまったく同様に、newでインスタンスを生成して利用します。これはJavaScriptから直接扱えるという意味でもあり、TypeScriptでライブラリを作ると、ほかのJavaScriptのコードからもシームレスに呼び出せます。

TypeScriptではメソッドやプロパティにprivate修飾子を付けて宣言すると、クラスの外部から操作ができなくなります。ただし、JavaScript自体にはそのような機構はないので、これはTypeScriptのコードとして処理しているときのみ意味を持ちます。つまり、ほかのJavaScriptのコードからはprivateなメソッドやプロパティにアクセスできてしまうので、注意が必要だということです。

ほかのクラスベースの言語と同じように、TypeScriptでも継承が行えます。次の例では、Greeterクラスを継承したAisatsuクラスを定義して、sayHelloメソッドを上書きしています。

class Aisatsu extends Greeter {
    sayHello(): void {
        alert('Konnichiwa! ' + this.message);
    }
}

ところでコードを書いていると、親クラスとして渡されたものを子クラスとして扱いたい場合が出てきます。たとえば次のコードのような場合です。

// canvas要素を取ってくる
var canvasE = document.querySelector('canvas');
// Canvas 2D Contextを取得する
var ctx = canvasE.getContext('2d');

このコードはJavaScriptとしては問題なく動作しますが、TypeScriptではコンパイルエラーになります。これは厳密に型をチェックしているためです。

まず、document.querySelector('canvas')はドキュメント内のcanvas要素を返しますが、querySelectorメソッドの戻り値は要素を表す汎用的なElement型です(つまり、変数canvasEElement型です⁠⁠。この値に対してgetContextメソッドを呼び出そうとしても、当然Element型にはそのようなメソッドはないのでコンパイルエラーになります。

この場合は型のキャスト(型変換)を行う必要があります。Element型ではなくgetContextメソッドを持つ下位のHTMLCanvasElement型であると認識できればよいのです。キャストするには、リスト3のコードのように<TypeName>を変数や戻り値の前に置きます。このコードでは、document.querySelectorの戻り値をHTMLCanvasElement型にキャストしています。

リスト3 キャストの例
// canvas要素を取ってくる
var canvasE = <HTMLCanvasElement>document.querySelector('canvas');
// Canvas 2D Contextを取得する
var ctx = canvasE.getContext('2d'); // ← OK

キャストのエラーは最終的に実行されたときに発生するので注意が必要です。リスト3のコードでは、canvas要素の実体がまさにHTMLCanvasElement型なので正常に動作しますが、たとえばまったく違う要素div要素など)HTMLCanvasElement型にキャストすると、実行時にどこかでエラーになります。

モジュール

TypeScriptにはモジュールと呼ばれるしくみがあります。モジュールには外部モジュールと内部モジュールがありますが、ここでは内部モジュールを説明します。モジュールとは簡単に言えばクラスを特定のかたまり(モジュール)に分割して管理することを支援するためのしくみです。クラスが多くなる場合や、ライブラリを作るうえでは必須とも言える機能です。リスト4のコードにはGreeterというクラスが2つ出てきますが、これらは異なるクラスです。

リスト4 モジュールの例
module Sample.Module1 {
    export class Greeter {
        hello(): void { alert('Hello!'); }
    }
}
module Sample.Module2 {
    export class Greeter {
        hello(): void { alert('Konnichiwa!'); }
    }
}
new Sample.Module1.Greeter().hello(); // => Hello!
new Sample.Module2.Greeter().hello(); // => Konnichiwa!

モジュールを定義するにはmodule ModuleName {…… }というブロックでモジュールに含めたいコードを囲みます。デフォルトではモジュール内で定義されたクラスはそのモジュールで閉じていて、外部からはアクセスできません。外部からアクセスしようとすると、コンパイルエラーになります。公開して外部からアクセス可能にする場合は、classなどの定義の前にexport修飾子を指定する必要があります。

module Sample.Module3 {
    class Greeter {
        hello(): void { alert('Hi!'); }
    }
}
new Sample.Module3.Greeter().hello();
// => classをexportしていないのでコンパイルエラー

インタフェース

TypeScriptにはインタフェースと呼ばれるしくみがあります。インタフェースは、簡単に言えば「こんなメソッドやプロパティたちを持っている」ことを表す定義です。

たとえば、次のコードはmessageプロパティを持つインタフェースを指定したクラスを定義しています。

interface IGreeting {
    message: string;
}
class Hello implements IGreeting {
    message = 'Hello!';
}
class Konnichiwa implements IGreeting {
    message = 'Konnichiwa!';
}
function say(greeting: IGreeting): void {
    alert(greeting.message);
}

say(new Hello()); // => Hello!
say(new Konnichiwa()); // => Konnichiwa!

インタフェースはクラスの宣言の一部として指定でき、特定のメソッドやプロパティを持つことを強制できます。このコードのclass Hello implements IGreetingという宣言は、⁠クラスHelloIGreetingインタフェースのメソッドを持つ必要がある」ということを意味します。実際、Helloクラスからmessageプロパティを削除するとコンパイルエラーになります。

代入の互換性

TypeScriptのインタフェースやクラスは、プロパティの型やメソッドのシグネチャが同じか要求に足りているなどの互換性があれば、明示的に型を宣言していなくても代入可能です。これは少し特徴的な点です。たとえばリスト5のコードはTypeScriptとして問題なくコンパイルできて実行できます。

リスト5 代入に互換性がある例
interface IGreeting {
    message: string;
}
function say(greeting: IGreeting): void {
    alert(greeting.message);
}
// IGreetingと同じプロパティを持つがIGreetingの実装を明示的に宣言していない
class Hi {
    message = 'Hi!';
}
// HiクラスはIGreetingとして扱える
say(new Hi()); // => Hi!
// 匿名オブジェクトはstring型のmessageプロパティがあるのでIGreetingとして扱える
say({ message: 'Ohayo-gojyaimasu!' }); // => Ohayo-gojyaimasu!

一方、次のコードのように互換性がない場合はコンパイル時にエラーとなります。1行目は匿名オブジェクトのmessageプロパティの型がstringではないのでエラーになり、2行目は匿名オブジェクトにmessageプロパティがないのでエラーになります。

say({ message: 1 });
say({ msg: 'Hoge!' });

その他の機能

TypeScriptには、ここまでに紹介した特徴以外にもさまざまな機能があります。

  • ジェネリクス
  • プロパティ(getter/setter)
  • 列挙型(Enum)
  • インデクサ
  • import対応

機能は多いですが、そのぶん使いこなせるととても強力で、開発効率を高めるツールとなるでしょう。

外部ライブラリと定義

ここまでTypeScript自体の紹介をしてきましたが、最後に実際にアプリケーションを開発するときには必ずと言ってよいほど必要になるライブラリとその使い方を紹介します。

TypeScriptを利用した開発で使うライブラリは、ほとんどの場合JavaScriptで書かれたものです。たとえばブラウザであればjQueryやKnockout.js、AngularJS[2]など、Node.jsであればSocket.ioやexpressなどです。これらのライブラリはJavaScriptで書かれているため、TypeScriptの最大の特徴である厳密な型を持っていないという問題があります。

そこでTypeScriptには、ソースコードとは別に型定義のみを記述して参照するしくみが用意されてます。型定義ファイルは.d.tsという拡張子を持ち、TypeScriptのソースコードからは特殊なコメントを書くことで参照できます。型定義ファイルを参照すれば、JavaScriptのライブラリも型が定義されているように扱えます。つまり、IDEのサポートを受けたりコンパイル時のチェックを受けたりできるので、TypeScriptで書かれたコードの延長のように扱えるのです図6⁠。

図6 Visual Studio上でjQueryの補完ができる
図6 Visual Studio上でjQueryの補完ができる

しかし、開発者がいちいち型定義ファイルを作成するのは現実的ではありません。有名なライブラリの型定義ファイルを集約しているDefinitelyTypedというコミュニティプロジェクトがGitHubにあり、jQueryやKnockout.jsといった数多くのライブラリの型定義ファイルが集められています[3]⁠。VisualStudioであればNuGetから、Node.jsであればnpmパッケージからtsdコマンドをインストールすれば簡単に取得/利用できます。

まとめ

ほんの触り程度の紹介しかできませんでしたが、TypeScriptは奥が深く、非常に実用的な言語です。静的型付け言語である点と開発コミュニティが活発な点でとても強力な言語だと思いますので、規模が大きい、または大きくなる可能性があるアプリケーションや、保守が続くアプリケーションの開発にぜひ採用してみてはいかがでしょうか。

提供/株式会社グラニ

http://grani.jp/

おすすめ記事

記事・ニュース一覧