TypeScript(プレビュー版)機能解説
TypeScriptの目玉機能「ジェネリック(Generics)」はこうなっている
JavaScriptのスーパーセットである「TypeScript」。その言語機能として追加されたものの中で、特に注目度が高い「ジェネリック(Generics)」の言語仕様や機能内容を紹介。
2013年のTypeScriptの進化の中で比較的大きな機能強化といえば、ジェネリックを置いて他に無い。ジェネリックとは、いったい何だろうか。なぜ重要なのだろうか。まずそこから説明を始めよう。
型を抽象化するジェネリック
C#などの知識があり、「ジェネリックの機能」と意味を分かっている読者は、ここを読み飛ばしてもよい。明確な型の概念を持たないJavaScriptを使ってきて、型の扱いにまだ慣れていない読者のために、簡単に「ジェネリックの意義」を説明してみよう。
ジェネリックは簡単に言えば、型引数を使用して、実際に利用されるまで型が確定しないクラスや関数を実現するためのものだ。
しかし、型引数とは何だろうか。関数の引数とは違うものだろうか。
簡単に引数をおさらいしてみよう。例えば、以下のようなコードがあったとする。
function a() {
alert("BELTLOGGER");
}
function b() {
alert("9");
}
a();
b();
|
値が違うだけで文字列を出力する(上記の例では「BELTLOGGER」と「9」)という同じ機能を持つ関数が2つもあるのは無駄だ。
これらを1つに統一したいときは引数を使用する。以下の例はx:stringが引数だ。
function a(x: string) {
alert(x);
}
a("BELTLOGGER");
a("9");
|
では以下のコードで、2つの関数を1つにまとめたいときはどうすればよいのだろうか。
function a(x: string) {
alert(x);
}
function b(x: number) {
alert(x);
}
a("BELTLOGGER");
b(9);
|
最初の例で、2つの関数を1つにまとめられたのは、引数を利用したからである。引数を利用できたのは、値が異なっていても型は同じstring型だったからである。ところが、今回の事例は型が異なっている(string型とnumber型)。
この問題はany型を使用すれば解決できることをご存じの読者もいらっしゃるだろう(次のコード)。
function a(x: any) {
alert(x);
}
a("BELTLOGGER");
a(9);
|
しかし、any型は「型チェックの放棄」を意味する。できれば使いたくはない。
この問題はオーバーロードを使用すれば解決できることをご存じの読者もいらっしゃるだろう(次のコード)。
function a(x: string);
function a(x: number);
function a(x: any) {
alert(x);
}
a("BELTLOGGER");
a(9);
|
こうすれば確実に引数の型はstring型かnumber型に限定可能だ。しかし、たった2つだけに限定されてしまうのは本意ではないかもしれない。本当はもっと他の型も使いたいのかもしれない。
このような場合には、次に示すように、型引数を導入してジェネリックで解決することができる。
function a<T>(x: T) {
alert(x);
}
a<string>("BELTLOGGER");
a<number>(9);
|
このように書くと、「a<string>」の引数は文字列だけ。「a<number>」の引数は数値だけが許される。また、もっと他の型も利用可能で、「a<Date>」と書くと日付時刻だけが許される。つまり、以下のようなコードも許されることが、string型とnumber型以外は許さないオーバーロード利用例と異なる点だ。
a<Date>(new Date());
|
さて、上記のサンプル「型引数を導入してジェネリックで解決した例」における「T」が型引数だ。「x」は引数で任意の値が入るが、「T」は型引数で任意の型が入る。どのような型を扱うか分からないクラスや関数に使用すると大きな効果を発揮する。
つまり、以下のような行は、
function a<T>(x: T)
|
「a<string>」として使用する場合は以下のように解釈される。
function a(x: string)
|
「a<number>」として使用する場合は以下のように解釈される。
function a(x: number)
|
「a<Date>」として使用する場合は以下のように解釈される。
function a(x: Date)
|
ジェネリックの基本構文
関数の場合の構文はすでに見たが、クラスなどに付加する場合は以下のように記述する。
class MyPerson<T>{
public name: T;
}
|
この場合、メンバー「name」の型は型引数で利用時に決定される。
さて、型引数は2つ以上あってもよい。その場合は以下のようにカンマで区切って列挙する。
function a<T, U>(t: T, u: U) {
alert(t);
alert(u);
}
a<string, number>("Fighter", 1);
|
型引数の名前には「T」を使用することが多いが、2つ以上の場合はアルファベット順に「T」「U」「V」や、「T1」「T2」「T3」などが慣習的に使用される。
さて、型引数は引数ではなく戻り値に使用してもよい。だから以下のようなコードも可能である。
function a<T>(t: T): T {
return t;
}
alert(a<number>(1234));
|
さらに言えば、型引数は関数の内部で使用するだけでもよい。以下のようにローカル変数の型を指定するためにのみ使用してもよい。
function a<T>() {
var x: T = null;
alert(x);
}
a<number>();
|
ジェネリックの型推論
以下のプログラムを実行すると、false/trueを出力する。
function a<T>(x: T) {
alert(x instanceof Date);
}
a<string>("new Date()");
a<Date>(new Date());
|
つまり、最初の呼び出しでは引数がDate型のインスタンスではないことを示し、2回目の呼び出しではDate型のインスタンスであることを示す。
このコードは実は、以下のように型名を省いて書き直しても同じ結果になる。
function a<T>(x: T) {
alert(x instanceof Date);
}
a("new Date()");
a(new Date());
|
つまりコンパイラに推論可能であれば、型名をいちいち指定する手間から解放される。
ただし、以下のようなケースでは推論させる意味はない。変数「y」の型はDate型ではなくany型になってしまうからだ。
function a<T>(x: T): T {
alert(x);
return x;
}
function b(x) {
var y = a(x);
}
b(new Date());
|
この場合、関数「b」の引数「x」の型は指定されていないのでany型となる。そのため、関数「b」から呼び出している関数「a」の引数もany型が推定される。つまり、関数「a」の仮引数にはany型が推定されてしまう。実際に渡すのはDate型なので、「Date型として推定してほしい」と思っていても、そうはならない。コンパイラの推定は万能とは言えないのである。
コレクションとしての例
コレクションにジェネリックを使用した簡単な例を見てみよう。
ここで使用する題材は、非常に単純化したリンクリストである。値を格納するノードが片方向のリンク情報でつながっている。途中でノードを挿入するときはリンク情報を置き換えるだけでよいので効率よく挿入ができる特徴がある。しかし、ノードに格納するデータの型は一様ではないので、ジェネリックを使用する価値がある、
以下は「one」「two」「three」という3つの文字列を入れた後で、「inserted」というデータを途中に挿入している。挿入の結果、「one」「two」「inserted」「three」という順番でデータが並ぶ例である。
class LinkedListNode<T>{
public next: LinkedListNode<T> = null;
public value: T;
}
class LinkedList<T>{
private startNode: LinkedListNode<T>;
public insert(insertAt: LinkedListNode<T>, newValue: T): LinkedListNode<T> {
var newNode = new LinkedListNode<T>();
newNode.value = newValue;
newNode.next = insertAt.next;
insertAt.next = newNode;
return newNode;
}
public append(newValue: T): LinkedListNode<T> {
if (!this.startNode) {
this.startNode = new LinkedListNode<T>();
this.startNode.value = newValue;
return this.startNode;
}
var tail = this.startNode;
while(tail.next) tail = tail.next;
return this.insert(tail, newValue);
}
public foreach(callback: (item: T) => void): void {
var p = this.startNode;
while (p) {
callback(p.value);
p = p.next;
}
}
}
var list = new LinkedList<string>();
list.append("one");
var two = list.append("two");
list.append("tree");
list.insert(two, "inserted");
list.foreach((item) => alert(item));
|
このように、「データの入れもの」という機能に特化したクラスを作るとき、ジェネリックは無類の強さを発揮する。
しかし、裏を返せば、このような使い方を続ける限り、データの入れものとしてしか使用できない。型引数「T」は、あくまでどのような型になるか予測できない仮の型であるから、型に属する機能は何も呼び出せないのである。この問題を解決するには、もう1つ別の機能を必要とする。
ジェネリックの制約
単なる入れものを脱却して、型引数で型付けされたデータを本格的に利用しようとすると、急に壁にぶつかる。
例えば、以下は型引数で型付けされたデータからsayMyNameメソッドを呼び出そうとした例だ。
interface X {
sayMyName();
}
class Y implements X {
public sayMyName() {
alert("I'm Big-Boy");
}
}
function a<T>(t: T) {
t.sayMyName();
}
a(new Y());
|
しかし、このコードはコンパイルできない。
型引数「T」はあくまで未知の型なのだ。そこにsayMyNameメソッドが存在するのかは予測できない。
このコードをコンパイル可能にするにはどうすればよいのだろうか。
any型を経由することで、あらゆるコンパイラの制約は回避できる(次のコード)。
function a<T>(t: T) {
var anyt: any = t;
anyt.sayMyName();
}
|
しかし、これは得策とは言えない。any型を利用すると型チェックが一切行われないからだ。
この場合は、型引数に制約を追加するとうまくいく。つまり、型引数に補足的な情報を追加すればよい。具体的には型引数を以下のように書き直せばよい。
function a<T extends X>(t: T) {
t.sayMyName();
}
|
この場合の「<T extends X>」とは、型引数「T」は型「X」ないし「Xを継承した型」に限るという意図を示している。それにより、少なくとも型「X」が提供するメンバーは必ず存在すると仮定でき、sayMyNameメソッドの呼び出しが可能になる。
このコードは以下のように書き直せる。この場合は、型「X」にはsayMyNameメソッドしか含まれていないので、「X」を指定しても大差ないが、利用したいメンバーが存在することだけ強制するつもりなら、そのメンバーだけを明示した型を直接書き込むことができる。
function a<T extends { sayMyName(); }>(t: T) {
t.sayMyName();
}
|
TypeScriptの型システムはダックタイピング的に型の一致を判定する。つまり、同じメンバーがあれば互換性のある型と見なす。他のメンバーは関係なければ無視される。そのため、このような指定により、利用したいメンバーそのものが含まれていれば、他の詳細は無視するように指定できる。
例えば以下の例では、「x」と「z」だけ利用して「y」は利用しないなら、「extends { x: number; z: number; }」のような制限を付けることができる。
class X {
public x: number;
public y: number;
public z: number;
}
function a<T extends { x: number; z: number; }>(t: T) {
alert("x+z is " + (t.x + t.z));
}
var b = new X();
b.x = 1;
b.y = 2;
b.z = 3;
a(b);
|
この場合、関数「a」からは、メンバー「y」に手出しをすることが基本的にできない。仮引数「T」を経由して「存在するはずだ」と明示されたのは「x」と「z」だけだからである。
ちなみに、この関数「a」には「x」と「z」さえあればどのような型でも指定可能である。「y」は無くても指定できる。
まとめ
なぜこのような機能がTypeScriptには必要とされているのだろうか。
恐らく、10~100行程度の小さなプログラムで必要とされる可能性はあまりない。つまり、TypeScriptの機能を学習している段階で必要性を感じる可能性は低い。しかし、コードのサイズが千行、万行と増えていくと、出番が生じる可能性が高くなる。コードの量が増えていくと、どうしても「メンテナンスの都合で「似たようなコードをまとめてコードをコンパクトにしたい」という欲求が出てくるからだ。
例えば「この2つの機能は型を除けばそっくりな処理を行っている」というケースに出会うことがある。そういうときに、コードを短くまとめるには、型を抽象的に扱う機能が必要とされる。それがTypeScriptの場合はジェネリックである。
最後に本稿の内容を箇条書きでまとめておこう。
- ジェネリックは、使用されるまで不明の任意の型を利用する技術
- 型引数が、任意の型に対応する
- 値が変化するときは引数。型が変化する時は型引数で関数をまとめられる
- 引数は関数に付けるものだが、型引数はクラスやインターフェースにも付けられる
- 型引数は複数あってよい
- 型引数は、引数、戻り値の型指定、関数やクラスの内部にも使用できる
- 型引数の型はコンパイラが推論してくれるが、頼りすぎは禁物
- 型には制約を付けることができる
- ジェネリックは、コレクションと相性がよい
- 一般的に、コードサイズが大きくなっていくと、ジェネリックの出番が増えていく