AltJSとTypeScriptとは
昨今、高度なWebアプリケーションやNode.jsによるサーバサイドアプリケーションなど、JavaScriptによるアプリケーションの開発が多く見られるようになってきました。それに伴い、アプリケーション開発の効率化のためのAltJSと呼ばれる言語(コンパイルするとJavaScriptを書き出す非JavaScript言語)たちが注目され、開発が盛んに行われています。TypeScriptはAltJSの1つです。
TypeScript以外にもさまざまなAltJSがあります。たとえばLL(Lightweight Language )を意識した書きやすいJavaScriptとしてのCoffeeScript[1] 、国産で速度最適化を重視しているJSX 、JavaScript以外の言語への変換ができる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のコンパイルを実行した際にエラーが出た例
これはリファクタリングなどを行う場合にもメリットになります。たとえば特定のクラスのメソッド名を変更した場合、利用している個所の修正漏れがあればコンパイル時にエラーが発生するので、漏れを見逃しにくくなります。コードの規模が大きくなればなるほど、その恩恵を受けられるでしょう。
Visual StudioやWebStormなどのIDEとの相性の良さ
Visual StudioやWebStormといったいわゆるIDE(Integrated Development Environment )のエディタを利用すれば、コーディング時にサポートを手厚く受けられるというのもTypeScriptの大きなメリットの1つです。特に開発元が同じMicrosoftということもあり、Visual Studioのサポートはとても手厚いです。
IDEのサポートで代表的なものと言えばコード補完ではないでしょうか。Visual StudioやWebStormでも、コード補完機能でメソッド名やプロパティ名が補完されます。これによりタイピングやリファレンスを探す手間が省けます(図2 ) 。
図2 エディタ上でのコード補完の例
またコードのエラーをエディタ上に表示する機能もあります。常にコードの状態を確認できるので、コンパイル前にミスに気づくことができます(図3 ) 。
図3 WebStormで文法エラーのチェックが働いている様子
IDEらしい強力な機能としては、リファクタリングサポートもあります。たとえばメソッド名を変更するとき、1ヵ所名前を変更すればそのメソッドを参照している名前をすべて書き換えてくれる機能があります(図4 ) 。
図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ではコード補完やシンタックスハイライトなどが利用可能
また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
コマンドを実行すると、引数に指定した.ts
ファイルと同名の.js
ファイルが生成されます。しかし、開発中に何度も書き換える場合、編集するたびにtsc
コマンドを実行するのは面倒です。そのため、tsc
コマンドには変更を監視して自動でコンパイルする--watch
オプションが用意されています。
--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
型です(つまり、変数canvasE
はElement
型です) 。この値に対して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
という宣言は、「 クラスHello
はIGreeting
インタフェースのメソッドを持つ必要がある」ということを意味します。実際、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の補完ができる
しかし、開発者がいちいち型定義ファイルを作成するのは現実的ではありません。有名なライブラリの型定義ファイルを集約しているDefinitelyTypedというコミュニティプロジェクトがGitHubにあり、jQueryやKnockout.jsといった数多くのライブラリの型定義ファイルが集められています [3] 。VisualStudioであればNuGetから、Node.jsであればnpm
パッケージからtsd
コマンドをインストールすれば簡単に取得/利用できます。
まとめ
ほんの触り程度の紹介しかできませんでしたが、TypeScriptは奥が深く、非常に実用的な言語です。静的型付け言語である点と開発コミュニティが活発な点でとても強力な言語だと思いますので、規模が大きい、または大きくなる可能性があるアプリケーションや、保守が続くアプリケーションの開発にぜひ採用してみてはいかがでしょうか。