型変数 (type variables)
ここでは、ジェネリクスで重要な概念となる「型変数」とは何なのかについて説明します。
型変数
「変数」といえば、値を代入する入れ物をイメージすることでしょう。たとえば、次のコードはxが変数で、その値に1を代入しています。
tsconstx = 1;
tsconstx = 1;
一度変数を定義すれば、あちこちに値を書き写す必要がなくなり、その変数を使ってさまざまな処理をすることができます。
変数という便利な入れ物があるおかげで、繰り返し同じコードを書く必要がなくなったり、より抽象的なコードを書けるようになったりと、さまざまな恩恵を享受できるわけです。
型変数(type variables)は、もうひとつの便利な入れ物です。ただし、入れられるのは「値」ではなく「型」という違いがあります。
tsfunctionprintAndReturn <T >(value :T ):T {console .log (value );returnvalue ;}
tsfunctionprintAndReturn <T >(value :T ):T {console .log (value );returnvalue ;}
このprintAndReturn関数のTが型変数です。<T>が型変数名を決めている部分でvalueの型に使われているTと戻り値に書かれているTは変数を利用している部分、つまり、参照している部分です。
printAndReturn関数のTの変数スコープは、この関数の範囲になります。したがって、関数のシグネチャでTを参照できるのはもちろん、関数の処理部分でも参照することができます。一方で、関数の外から参照することはできません。
tsfunctionprintAndReturn <T >(value :T ):T {letvalues :T [] = []; // OKconstdoSomething = (value :T ) => {}; // OKreturnvalue ;}letCannot find name 'T'.2304Cannot find name 'T'.value :; T
tsfunctionprintAndReturn <T >(value :T ):T {letvalues :T [] = []; // OKconstdoSomething = (value :T ) => {}; // OKreturnvalue ;}letCannot find name 'T'.2304Cannot find name 'T'.value :; T
この関数を利用するコードは、numberなどTに好きな型を代入することができます:
tsconstvalue =printAndReturn <number>(123);
tsconstvalue =printAndReturn <number>(123);
型引数
型引数(type arguments)とは、型変数に代入した型のことを言います。次のコードではnumberが型引数です。
tsconstvalue =printAndReturn <number>(123);
tsconstvalue =printAndReturn <number>(123);
TypeScriptでは、型引数にも型推論が行われます。型引数推論(type argument inference)と言われます。上の例では、型変数Tにnumberを代入するコードを明示的に書いていますが、変数の123から型変数Tの型はnumber型になることがコンパイラからは推測可能なので、次のコードのように型引数の記述を省略することもできます。
tsconstvalue =printAndReturn (123);
tsconstvalue =printAndReturn (123);
型変数に使える文字
TypeScriptの型変数に使える文字は変数名や関数名、クラス名などに使える文字種と同じものが使えます。したがって、大文字アルファベット1文字でなければならないといった制約はありません。
tsfunctionfunc1 <T >(x :T ) {}functionfunc2 <Foo >(x :Foo ) {}functionfunc3 <fooBar >(x :fooBar ) {}functionfunc4 <$ >(x :$ ) {}functionfunc5 <かた >(x :かた ) {}
tsfunctionfunc1 <T >(x :T ) {}functionfunc2 <Foo >(x :Foo ) {}functionfunc3 <fooBar >(x :fooBar ) {}functionfunc4 <$ >(x :$ ) {}functionfunc5 <かた >(x :かた ) {}
このように、3文字のFoo、キャメルケースのfooBar、記号の$、Unicodeのかたも型変数名として定義可能です。
tsfunction func1<1>(x: 1) {} // コンパイルエラーfunction func2<class>(x: class) {} // コンパイルエラー
tsfunction func1<1>(x: 1) {} // コンパイルエラーfunction func2<class>(x: class) {} // コンパイルエラー
型変数名に使えないものは数字ではじまるもの、classなどの予約語です。
型変数名の慣習
TypeScriptの慣習として、型変数名にはTを用いることが多いです。このTはtemplateの略と言われています。
単純なジェネリクスで、型変数が2つある場合は、TとUが用いられることがあり、その理由はアルファベット順でTの次がUだからです。この規則にしたがって、3つ目の型変数はVとする場合もあります。
tsfunctioncompare <T ,U >(a :T ,b :U ) {}
tsfunctioncompare <T ,U >(a :T ,b :U ) {}
複数の型変数がある場合、型変数に意味のある名前をつけることもあります。その場合、TKey、TValueのようにT接頭辞を付けた命名規則がしばしば使われます。ただし、これは「型変数名にはTを用いる」という慣習ほどは一般的でないように思われます。
tsfunctionmakeKeyValuePair <TKey ,TValue >(key :TKey ,value :TValue ) {}
tsfunctionmakeKeyValuePair <TKey ,TValue >(key :TKey ,value :TValue ) {}
型変数名を単語にする場合は、大文字始まりのキャメルケースにすることが普通です。
型変数の名づけを巡る議論
「型変数はどのような名づけがベストか?」というテーマについては、プログラマーによってさまざまな主張があり、見解が分かれるところです。ここでは、参考までにその議論を取り上げますが、どうか混乱しないで頂ければと思います。実務においては、プロジェクトで一貫した命名規約に準ずることを意識しておけばいいでしょう。
できるだけ意味のある単語にすべきという主張
Tのような1文字だけでは何が入るのか分かりにくいため、意味のある単語にすべきという考え方があります。Array<T>の代わりに、Array<Element>のほうが分かりやすいとする立場です。
1文字のほうがよいとする主張
ジェネリクスはそもそも抽象的なものごとを扱うため、具体的な名前が付けられないため、意味を持たないTのほうがいいという考え方があります。
Elementのような型変数名を定義すると、一見するとそれがクラス名のように見えてしまいます。混乱を避けるためにもTのほうがよいという意見があります。
型変数のスコープの広さによって使い分けるべきとする主張
プログラミングでの名づけは、名つけるもののスコープが広くなるほど長い名前を、狭くなるほど短い名前をつけるというテクニックがあります。たとえば、forループで変数にiを用いることがありますが、これは慣習というところもありますが、変数のスコープが狭いという一面もあります。型変数にも同じことが言えて、型変数のスコープが広いものは単語にし、短いジェネリック関数の中でしか使わない型変数は1文字が妥当という考え方があります。