はじめに
Angularの次期バージョンがTypeScriptで開発されるなど、なにかと話題のTypeScriptですが、いつのまにかバージョンも1.5になっています。このままではビッグウェーブに乗り遅れてしまう!と、エンジニアとしての危機感が目覚め、まずは言語仕様をマスターするんやで〜とさっそく公式サイトの言語仕様書をクリックしてみましたが170ページもあり挫折。。。気をとりなおして、Introduction部分だけでも読んでみました。
本記事ではTypeScriptの言語仕様書のIntroductionの構成に沿って、TypeScriptの紹介をしてみたいと思います。ただし、言語仕様書の完全な訳ではなく、自分なりの解釈や感想が入っていますので注意してください。
補足ですが、Playgroundと呼ばれるTypeScriptの実行環境がWebサービスとして提供されています。Playgroundではブラウザ上で簡単にTypeScriptのコードを書いて実行できます。簡単にとはいえ、型チェックや型推論などもされる本格的な実行環境です。Introductionを読みながら、私も実際にPlaygroundでコードを書いて試してみましたが文字通り遊べて楽しいです。
構成
Introductionの構成は以下のようになっています。まずは、TypeScriptの特徴や概要が簡単に触れられています。
- TypeScriptの特徴
概要紹介のあと、TypeScriptの機能が各セクションごとにまとめられています。以下は、Introductionの目次の抜粋です。この目次だけでもTypeScriptがもつ機能がなんとなく分かる気がします。
- Ambient Declarations (アンビエント宣言)
- Function Types (関数型)
- Object Types (オブジェクト型)
- Structural Subtyping (構造的部分型)
- Contextual Typing (コンテキストに基づく型)
- Classes (クラス)
- Enum Types (列挙型)
- Overloading on String Parameters (String型引数のオーバーローディング)
- Generic Types and Functions (ジェネリック型とジェネリック関数)
- Module (モジュール)
TypeScriptの特徴
Introductionの冒頭でTypeScriptの特徴が以下のように列挙されています。
- TypeScriptはJavaScriptのシンタックスシュガーである。
- TypeScriptはEcmaScript5 (ES5) のスーパーセットである。
- したがって、JavaScriptのプログラムはTypeScriptのプログラムである。
- TypeScriptのコンパイラはTypeScriptのプログラムをJavaScriptのプログラムに変換するトランスレータである。
- TypeScriptはEcmaScript6 (ES6) で提案されているいくつかの機能(クラスやモジュールなど)を先取りしている。
- TypeScriptは型付けされた言語であり、コンパイラは型の整合性を静的にチェックする。
- 型アノテーションが省略されている場合、その型はコンパイラによって自動的に推論される。
最後の2点から、ScalaやHaskellなどと同じく、TypeScriptは型推論可能な静的に型付けされた言語であることが分かりますね。TypeScriptでは、変数宣言に以下のように型アノテーションを記述します。コロンを使ってScalaっぽい書き方ですね。
//string型の変数宣言
var helloWorld:string = "Hello, TypeScript."
TypeScriptのコンパイラは型チェックを行います。そのため、上で宣言したstring型の変数をnumber型の変数に代入しようとすると、コンパイラが型エラーだと警告してくれます。
//string型変数をnumber型変数に代入しているため、型エラーとなる
var numvar:number = helloWorld;
型アノテーションを省略した場合、コンパイラによって型推論が行われます。以下のコードでは、変数helloWorldは型アノテーションがありませんが、文字列値により初期化しているため、コンパイラはhelloWorldの型をstringと推論します。推論されたstring型の変数をnumber型の変数に代入しているため、正しく型エラーが判定されます。
//文字列値で初期化しているので、helloWorld変数はstring型であることが推論できる
var helloWorld = "Hello, TypeScript.";
//型エラー
var numvar:number = helloWorld;
コンパイラが変数の型を推論できない場合、その型はAny型となります。JavaにおけるObject型のようなものでしょうか?Any型の変数は、その構造になんの制約もない何にでもなりえる変数です。例えば、次のコードでは、anyvar変数は型アノテーションも初期化値もないため、コンパイラはその型を推論できません。結果として、anyvarの型はAnyとなります。
//コンパイラはAny型を推論する
var anyvar;
//同じ変数に数値や文字列を代入してもエラーとならない
anyvar = 1;
anyvar = "one";
//どんなメソッドやプロパティを参照してもエラーとならない
anyvar.foo();
静的に型付けされた言語であるため、IDEによるコード保管が可能となります。以下はPlaygroundでのコード保管の様子です。string型変数の関数を自動的にsuggestしてくれます。これは、開発者にとっては非常に嬉しいですね。今のJavaScriptでもJSDocコメントを記述することでコード保管が可能なツールもありましたが、TypeScriptでは型が言語仕様に組み込まれているため、より強力で正確です。
TypeScriptのコンパイラはTypeScriptのコードから意味的に等しいJavaScriptのコードを生成するトランスレータであることが述べられています。では型付けされたTypeScriptのコードからどのようなJavaScriptのコードが生成されるのでしょうか?実際にコンパイラが生成するJavaScriptのコードが以下です。(Playgroundでは生成されたJavaScriptのコードも確認できるようになっています)
TypeScriptのコード:
var helloWorld:string = "Hellow, World.";
var numvar:number = 1;
function foo(s:string) {
return s;
}
生成されたJavaScript:
var helloWorld = "Hellow, World.";
var numvar = 1;
function foo(s) {
return s;
}
JavaScriptでは、TypeScriptの型アノテーションは完全に消去されていることがわかります。これは、Javaのジェネリクスとよく似ていますね。Javaのジェネリクスもコンパイル時に型チェックは行われますが、生成されたコード(クラスファイル)からは型の情報はすべて消去されます。一つ違う点は、 TypeScriptでは型エラーがあるプログラムでもコンパイルエラーとならずにJavaScriptを生成する 点です。これは、JavaScriptという言語がそもそも動的に型付けされた型にゆるい言語であるということと、TypeScriptをJavaScriptのスーパーセットとした言語仕様の選択(つまり既存のJavaScriptもTypeScriptでコンパイルできる)からも避けられないのでしょう。
Ambient Declarations (アンビエント宣言)
TypeScriptでは、宣言されていない変数を使用するとコンパイル時警告となります。
//fooは宣言されていないのでコンパイル時警告
foo.bar = "hoge";
外部ライブラリの変数(たとえばjQueryの$
)や暗黙に宣言されている変数については、以下のように宣言することで、変数を利用できるようになります。これをアンビエント宣言といいます。
//アンビエント宣言は外部ライブラリなどが宣言している変数fooを利用可能にする
//以下の例では、fooの型を宣言していないのでその型はAnyとなる
declare var foo;
foo.bar = "hoge";
JavaScriptの組み込み変数や、documentなどのDOMオブジェクトに関しては、あらかじめTypeScriptが宣言しているため、アンビエント宣言は不要です。lib.d.tsファイルで組み込み変数が提供されています。
Function Types (関数型)
関数型はパラメータの型のリストと、戻り値の型からなります。文字列を受け取り、文字列を返す関数の型リテラルは以下のように表現します。
(s:string) => string
上記の型を持つ関数は以下のように宣言できます。(ただし、以下の例では、リターン値の型から型推論により戻り値の型は推論可能であるため、戻り値の型アノテーションは省略可能です。)
function foo (s:string): string {
return s;
}
関数を引数とする関数は以下のように記述できます。twice
関数の2つめの引数は関数であり、string型を引数としてstring型を返すことが宣言されています。
//受け取った関数を2回実行する関数
function twice(s:string, f:(s:string) => string) {
return f(f(s));
}
//"a"が返る
var result = twice("a", foo);
Object Types (オブジェクト型)
オブジェクト型はオブジェクトの構造を定義します。次のコードは、オブジェクト型リテラルを使って、変数 user
を宣言します。クエスチョンマーク(?)が付いているプロパティはオプション(必須でない)となります。
var user: {
firstName:string;
lastName:string;
//プロパティ名に?が付いているとオプションになる
middleName?:string;
}
オブジェクト型には名前をつけることができます。名前付きのオブジェクト型を インターフェース(interface) と呼びます。たとえば、上記例のオブジェクト型をPersonという名前のインターフェースとして定義すると以下のようになります。
interface Person {
firstName:string;
lastName:string;
middleName?:string;
}
あるオブジェクトリテラルが、オブジェクト型Aが持つプロパティをすべて保持しているなら、そのオブジェクトリテラルは型Aを満たします。上記のPerson
を使ったコードで具体的に示してみます。
//Person型オブジェクトを受け取る関数
function greet(p: Person) {
console.log("hello, " + p.firstName + " " + p.lastName);
}
greet({firstName:"Augusta", middleName:"Ada", lastName:"King"}); //OK
greet({firstName:"Augusta", lastName:"King"}); //オプションのmiddleNameはなくてもOK
greet({firstName:"Augusta"}); //必須プロパティのlastNameがないので型エラー
greet({firstName:"Augusta", lastName:"King", foo:"foo"}); //余計なプロパティがあってもOK
JavaScriptのオブジェクトは、それ自身が関数としても振舞うことができます。たとえば、jQueryオブジェクトである$
は、ajaxを呼び出す関数getを持っています。また同時に$
自身は関数としても呼び出すことができます。以下は言語仕様書から引用した、jQueryの$
をTypeScriptのインターフェースでモデル化したコードです。
interface JQuery {
text(content: string);
}
interface JQueryStatic {
//ajaxコールを行うgetメソッド
get(url: string, callback: (data: string) => any);
//文字列からjQueryオブジェクトを生成する
(query: string): JQuery;
}
declare var $: JQueryStatic;
JQueryStatic
インターフェースにおいて、以下のように関数名を持たない関数型が指定されていることに注目してください。このような生の関数型が指定された場合、このインターフェースのインスタンス自身を関数として呼び出し可能なことを表現します。
//文字列からjQueryオブジェクトを生成する
(query: string): JQuery;
このように宣言されているため、JQueryStatic
型を持つ$
変数は以下のように文字列を引数として関数として呼び出しても型エラーとはなりません。
$("div").text("foo");
さらに、jQueryの$
変数は文字列ではなく、readyイベントハンドラ関数を引数としても呼び出すことができます。このような複数の振る舞いを保つ場合、生の関数型を複数指定することにより、関数をオーバーロード定義することができます。JQueryStatic
インターフェースに次の型定義を追加することにより、
//readyイベントハンドラを登録する
(ready: () => any): any;
関数を引数として$
を呼び出すことが可能となります。
$(function() {
console.log('ready');
});
Structural Subtyping (構造的部分型)
あるオブジェクトA
が、オブジェクトB
が持つプロパティと同じ名前と型のプロパティをすべて持つ場合、A
はB
のサブタイプとなります。オブジェクトの構造により決まる型付けを構造的部分型といいます。例えば、以下のPoint
型とColorPoint
型をみてください。Javaのextends
キーワードのように、これらの型には明示的な型関係は示されていません。にもかかわらず、ColorPoint
型はPoint
型と同じプロパティをすべて定義しているため、ColorPoint
型はPoint
型のサブタイプとなります。
interface Point {
x: number;
y: number;
}
interface ColorPoint {
x: number;
y: number;
color: string;
}
したがって、以下のコードが示すように、Point
型の値が要求されるところで、ColorPoint
型の値を適用することができます。
function plus(p1:Point, p2:Point) {
return {
x: p1.x + p2.x,
y: p1.y + p2.y
}
}
var colorPoint: ColorPoint = {
x: 0,
y: 0,
color: "blue"
}
//plus関数の引数はPoint型と宣言されているが、ColorPoint型の値を適用できる
var p = plus(colorPoint, {x:0, y:0});
動的型言語のRubyなどで有名なダックタイピングと似ていますね。ただし、ダックタピングは動的に型付けされるのに対し、構造的部分型は静的に型チェック点が異なります。
Contextual Typing (コンテキストに基づく型)
TypeScriptの型推論は通常ボトムアップに行われます。つまり、構文木の末端からルートに向かって型が決定されていきます。例えば、戻り値の型が宣言されていない、以下のfoo
関数を例にしましょう。引数a
とb
はnumber型です。number型同士の値に+演算子を適用した結果はnumber型となります。number型の値をreturnしているため、コンパイラはplus
関数の戻り型をnumber型と推論します。
function plus(a:number, b:number) {
return a + b;
}
型アノテーションがない変数や初期化されていない変数の型はany
となることはすでに説明しました。しかし、このような(型アノテーションのない)型の変数は、そのコンテキストによってトップダウンに型付けされる場合があります。これを コンテキストに基づく型 とよびます。以下のコードをみてください。twice関数は、第2引数に指定された関数を2回実行します。第2引数は関数型であり、その関数の唯一の引数はnumber型です。
function twice(n:number, f:(n:number) => number) {
return f(f(n));
}
さて、以下のようにこのtwice関数の2つめのパラメータとして関数リテラルを適用してみましょう。関数リテラルの引数n
の型は宣言されていません。しかし、twice関数の第2引数は(n:number) => number
型の関数型であるため、このコンテキストにもとづき、n
の型はnumber型と推論されます。
//引数nの型は宣言されていないが、tiwce関数の型宣言からnumber型であることが推論されます
var result = twice(0, function(n) {
return n + 1;
})
console.log(result); //2が出力されます
Classes (クラス)
プロトタイプベースのオブジェクト指向言語であるJavaScriptでは、オブジェクトの継承関係はプロトタイプチェーンにより実行時に決まります。一方、Javaのようなクラスベースのオブジェクト指向言語では、オブジェクトの継承関係はクラスによって静的に決定されます。TypeScriptでは、クラスベースにオブジェクトの継承関係を定義することができます。
TypeScriptでは、class
キーワードを使い、以下のようにクラスを定義します。コンストラクタはconstructor
キーワードで指定します。
class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
greet() {
return "Hello, I am " + name + ".";
}
}
プロパティはアクセス指定子private
、public
もしくはprotected
を付与することができます。それぞれ、クラス内部からのみアクセス可能、クラスとその継承先クラス内部からのみアクセス可能、どこからでもアクセス可能であることを意味します。アクセス指定しない場合は、public
となります。以下のように、コンストラクタの引数にプロパティのアクセス指定子を付与することで、プロパティの変数定義を省略することができます。プロパティの可視性はあくまで、コンパイル時にのみ強制されるものであり、(JavaScriptに変換後の)プログラム実行時にはなんら制約を持たないことに注意が必要です。
class Person {
constructor(public name: string) {
this.name = name;
}
greet() {
return "Hello, I am " + name + ".";
}
}
既存のクラスを継承することもできます。クラスの継承はextends
キーワードを使い、以下のように記述します。
class SleepingPerson extends Person {
constructor(name : string) {
super(name);
}
greet() {
return "I am sleeping...";
}
wakeUp() {
console.log('I woke up!');
}
}
サブクラスのコンストラクタからは、super
キーワードにより、継承元クラスのコンストラクタを呼び出せます。また、継承元クラスと同名のメソッドを定義することで、メソッドをオーバーライドすることもできます。ただし、メソッドの引数や戻り値の型は一致しなければなりません。また、上の例のように、サブクラス独自のメソッドを定義することも当然可能です。
Enum Types (列挙型)
TypeScriptでは列挙型を定義することができます。以下のコードは算術演算子を列挙型として定義する例です。
enum Operator {
ADD,
SUB,
MUL,
DIV
}
列挙型はコンパイラによってJavaScriptの定数値に変換されます。定数が明示的に指定されていない場合、自動的に0から始まる数値が自動的に割り当てられます。(定数値は明示的に指定することも可能なようです。)以下のように列挙型に[]演算子を適用すると、列挙値の文字列表現を取得できます。
console.log(Operator.ADD); //0が出力される
console.log(Operator[0]); //ADDが出力される
console.log(Operator[Operator.ADD]); //ADDが出力される
列挙値はswitch文で用いることができます。以下のコードは指定されたOperator型にもとづいて算術演算する関数です。
function evaluate(op: Operator, n1:number, n2:number) {
switch(op) {
case Operator.ADD: return n1 + n2;
case Operator.SUB: return n1 - n2;
case Operator.MUL: return n1 * n2;
case Operator.DIV: return n1 / n2;
}
}
var result = evaluate(Operator.ADD, 1, 2); //実行結果は3
Overloading on String Parameters (String型引数のオーバーローディング)
JavaScriptでは、引数の文字列値によって振る舞いが変わる関数がしばしば登場します。たとえば、documentオブジェクトのcreateElementメソッドは、引数として受け取ったタグ名に応じて、対応するDom要素を生成します。TypeScriptでは、引数の文字列の値によって型が変わるような関数を型付けすることができます。これを String引数型のオーバーローディング といいます。
以下のPlaygroundのスクリーンショットは、documentオブジェクトのcreateElement関数の戻り値の型が推論されている様子です。引数のタグ名によって対応するDom要素の型が推論されていることが分かりますね。
このような文字列値のオーバーローディングはどのように定義するのでしょうか?lib.d.tsファイルを見るとその答えがあります。documentオブジェクトのインターフェース定義で、createElementの型が以下のように記述されています。
createElement(tagName: "a"): HTMLAnchorElement;
createElement(tagName: "abbr"): HTMLPhraseElement;
createElement(tagName: "acronym"): HTMLPhraseElement;
//...続く
Generic Types and Functions (ジェネリック型とジェネリック関数)
TypeScriptはジェネリック型をサポートしています。以下のコードは、ジェネリック型であるList
の記述例です。型パラメータは、Javaのジェネリクスのように<>
で指定します。
interface List<T> {
elem: (i:number) => T;
add: (e:T) => void;
}
function test(list:List<string>) {
list.add("a"); //OK
list.push(1); //型エラー
var s = list.elem(0); //OK
var i:number = list.elem(0); //型エラー
}
ジェネリック関数を定義することもできます。上記のList
について、各要素に指定された関数を適用して新しいList
を生成するmap関数は以下のように定義できます。
interface List<T> {
elem: (i:number) => T;
add: (e:T) => void;
map<R>(f:(e:T) => R): List<R>;
}
map関数は型パラメータRでジェネリック化されています。map関数はリストの各要素に関数f
を適用し、その結果から新しいListを生成します。生成されたListの要素型は関数fの戻り値の型と同じRになります。
TypeScriptはジェネリック関数の型を推論してくれるため、明示的に指定する必要はありません。例えば、以下の例では、map関数に指定している関数の戻り値がstring型であるため、コンパイラはmap関数の型パラメータRはstring型であると推論可能です。
function test(list:List<string>) {
//型パラメータを明示的に指定
list.map<string>(function (e) {
return e.toUpperCase();
});
//map関数の型パラメータは不要。コンパイラが推論可能であるため。
list.map(function (e) {
return e.toUpperCase();
})
}
Module (モジュール)
JavaScriptでは、オブジェクトのプロパティに対する可視性を設定する方法はありません。オブジェクトのプロパティはすべて外部からアクセス可能となります。このため、クロージャにprivateな変数や関数を隠ぺいするパターン(モジュールパターン)がJavaScriptでは一般的です。TypeScriptでは、言語仕様としてモジュールパターンが提供されています。
TypeScriptでモジュールを定義する方法は以下のように記述します。module
キーワードでモジュールを定義します。モジュール内部でexport
キーワードを付与することで、そのメンバは外部からアクセス可能となります。
module M {
var privateVar: string = "private";
export var publicVar: string = "public";
function privateFunc() {}
export function publicFunc() {}
class PrivateClass {}
export class PublicClass {}
}
M.publicVar; //OK
M.privateVar; //エラー
M.publicFunc(); //OK
M.privateFunc(); //エラー
new M.PublicClass(); //OK
new M.PrivateClass(); //エラー
上記のモジュールの仕組みは internal module(内部モジュール)と呼ばれます。TypeScriptでは、 external module(外部モジュール)と呼ばれるもう一つのモジュール機構があります。これについては、仕様書を参照してください。