第11回 関数に関するいくつかのトピックTypeScriptで学ぶJavaScript入門(3/4 ページ)

» 2015年06月04日 05時00分 公開
[羽山博]

ジェネリックス

 ジェネリックスとは、データ型を仮に決めておき、実際に使用するデータ型を呼び出し時に変えられるようにする機能で、総称型とも呼ばれる。ジェネリックスは関数だけでなく、クラスなどでも使われるが、ここでは関数を例に取って説明する。

 ジェネリックスを利用すると、データ型を関数の呼び出し時に決められるので、似たような処理を異なるデータ型の引数に対して行うことができる。簡単な例で見てみよう。以下の例は、単に引数の値を返すだけの関数である。

function parrot<T>(data: T): T {  // (1)
  var ret: T; // (2)
  ret = data;
  return ret;
}
alert(parrot<number>(100)); // (3)
window.close();


 このプログラムを実行すると、結果は「100」と表示される。(2)の部分を以下のように書き換えると、結果は「abc」となる。

alert(parrot<string>("abc"));
alert(parrot<string>(123));    // データ型が合わないのでこれはエラーとなる

ジェネリックスを利用した関数の呼び出し例

 最初のコードに戻って書き方を見てみよう。(1)は関数の宣言だが、仮引数や戻り値のデータ型を、取りあえず「T」などの文字で表しておく。(2)以降も同様である。変数のデータ型を指定するために、取りあえず指定した「T」を使う。後は、関数名の後に、何を「取りあえず」のデータ型にしたかを「<>」に囲んで書いておく。図で表すと以下のようになる。

図5 ジェネリックスを利用する関数の作り方 図5 ジェネリックスを利用する関数の作り方
関数の中でデータ型を指定する箇所を「T」などの文字に変更し、関数名の後に「<>」に囲んでその文字を書けばよい。なお、この例では、仮引数dataをそのまま戻り値にしてもいいのだが、ジェネリックスの書き方が分かるように、あえて作業用の変数を関数の中で使っている。

 ジェネリックスを使った関数を呼び出すときには、関数名の後に「<>」を書き、その中に実際にどのデータ型を使うのかを書けばよい。(3)のコードを見てみよう(「alert(parrot<number>(100));」)。この場合は、「T」が「number」に置き換えられた関数が呼び出されることになる。

 ジェネリックスで、仮に指定するデータ型は、複数あってもよい。以下の例は、引数として与えられたデータの長さを返すものである(ただし、このままではまだ動かない)。

function getLength<T, U>(x: T): U { // (1)
  return x.length;  // (3)
}
alert(getLength<string, number>("総称型"));  // (2)
window.close();


 この例では、「T」と「U」という「仮のデータ型」を指定している。そして、「T」と「U」を使って仮引数はT型で、戻り値はU型であると指定しているわけだ。(1)で、関数名の後の「<>」の中に、「T」と「U」をカンマで区切って書いてあることに注目しよう。

 関数を呼び出すときには、やはり関数名の後に「<>」を書き、その中に実際のデータ型を書く。(2)では、「<string, number>」と書かれているので、「T」が「string」になり、「U」が「number」になる。

 ただし、このプログラムでは、(3)の部分でエラーになる。というのは、T型の変数にはlengthプロパティがないからである。string型であれば、lengthプロパティは使えるが、関数が呼び出されるまでは「T」の実際のデータ型が決まらないので、lengthプロパティが使えるかどうか分からないというわけだ。

 このような場合、以下のように「T型にはlengthプロパティがあるよ」と指定すればエラーにならない。この指定のことを「制約」と呼ぶ。

interface PROP {
  length: any// (1)
}
function getLength<T extends PROP, U>(x: T): U {  // (2)
  return x.length;
}
alert(getLength<string, number>("総称型"));
window.close();


 PROPというインターフェースでは、(1)のようにlengthというプロパティがあり、そのデータ型はanyであることを示している。これを利用して(2)のように、「extends」に続けてインターフェースの名前を指定してやれば、T型にlengthプロパティが含まれることになり、正しくプログラムが実行できるようになる。

 ただし、このプログラムはちょっとズルをしている。(1)では、本来ならnumber型を指定したいところだが、number型を指定するとまたもやエラーになってしまう。それは、U型にはnumber型の値を代入できない(かもしれない)からである。戻り値がどんな場合であっても、number型であると分かっているなら、別にジェネリックスを使う必要はない。以下のように書けばいいだけの話である。

interface PROP {
  length: number;
}
function getLength<T extends PROP>(x: T): number {
  return x.length;
}
alert(getLength<string>("総称型"));
window.close();


 落とし穴はまだある。lengthプロパティはstring型にしかないので、以下のように、number型の引数を指定してgetLength関数を呼び出すとエラーになるということだ。

alert(getLength<number>(123));

制約を加えると、その制約が当てはまらない場合にはエラーになる
number型にはlengthプロパティがないので、このような呼び出し方はできない。

 何とかうまく動くようにしようとして、最初の方で示した場合分けのコードを書き、数値の場合の桁数を数えようとしても、やはり<T>型は数値として扱えないなどのエラーが出てうまくいかない。

 つまり、ジェネリックスは、さまざまなデータ型の引数に対してきめ細かく処理を分けるためではなく、どのデータ型に対しても同じような処理をしたい場合に使うとよい(きめ細かく処理を分けたいのであれば、ジェネリックスを使うのではなく、関数をオーバーロードすればいい)。

 例えば、以下のように、どのようなデータ型の配列であっても、インデックスが限界を超えたときには最初に巻き戻して要素を取り出す、といった処理をするときにジェネリックスが適している。要素が四つしかない配列であれば、インデックスは0〜3までとなるが、4を指定したときには0に戻って0番目の要素を取り出す、といった処理になる。同様に、5を指定すれば1番目の要素が取り出される。

function getCircleArray<T>(data: T[], idx: number): T {
  var index: number;
  index = idx % data.length;  // (1)
  return data[index];
}
var a = [10, 20, 30, 40];
var dir = ["N", "E", "S", "W"];
alert(getCircleArray<number>(a, 5));  // (2)
alert(getCircleArray<string>(dir, 5));  // (3)
window.close();


 (1)では、引数で指定されたidx(取得したい値のインデックス)の値を配列のサイズで割った余りを求め、実際のインデックスとしている。仮引数dataというのは配列なので(どのようなデータ型の配列であっても)lengthプロパティが使える。(2)では、ジェネリックスを使って指定されたデータ型がnumber型であることを示してgetCircleArray関数を呼び出している。インデックスには5を指定しているが、配列のサイズが4なので、5を4で割った余り、つまり1番の要素が取り出されることになる(配列のインデックスは0から始まることを思い出してほしい)。当然のことながら、結果は20と表示される。(3)では、データ型をstring型として、getCircleArray関数を呼び出している。こちらの結果は「E」となる。

 最後にクロージャーと呼ばれる機能について紹介しよう。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。