ジェネリクス
イントロダクション
ソフトウェア工学の大部分は、明確に定義され一貫性のあるAPIを持つだけでなく、再利用性のあるコンポーネントを構築することにあります。 現在のデータでも将来のデータでも動作可能なコンポーネントは、 大規模なソフトウェアシステムの構築に欠かせない柔軟な機能を提供してくれることになるでしょう。
C#やJavaのような言語では、再利用可能なコンポーネントを作成するための主要なツールの1つにジェネリクス(generics)があります。 これは、1つの型ではなく様々な型で動作するコンポーネントの作成を可能にしてくれます。 ユーザーは任意の型でそれらのコンポーネントを使用することができます。
ジェネリクスの"Hello World"
始めるにあたり、identity関数にジェネリクスの"Hello World"をやらせてみましょう。
このidentity関数は、渡されたものをそのまま返す関数で、
echo
コマンドと同じようなものだと考えてください。
ジェネリクスが無い場合は、特定の型をidentity関数に指定するか、
function identity(arg: number): number {
return arg;
}
any
型を使用したidentity関数にするかの、どちらかでなければいけません。
function identity(arg: any): any {
return arg;
}
any
を使用すれば、引数の型にとして全ての型が受け入れられることが明確に指定されますが、
関数の戻り値の型の情報は失われます。
もし、数値(number)を渡しても、得られる情報はany
型が返るという情報だけです。
戻り値の型としても使用する出来るようにに、引数の型を捕捉する方法が必要です。 ここで、「値」ではなく「型」上で動作する特別な種類の変数である、「型変数 (type variable)」を使用してみます。
function identity<T>(arg: T): T {
return arg;
}
ここで、identity関数へ型変数T
を追加しています。
このT
はユーザーが提供した型(例: number)の捕捉を可能にするため、
この型情報を後で使用することができます。
ここでは、戻り値の型としてT
を再び使用しています。
検証のために、引数と戻り値の型に同じ型が使用されていることが確認できます。
これは、入る型と出て行く型の行き来の捕捉を可能にしてくれます。
このidentity関数は、型にとらわれずに動作するジェネリクスな関数であると言えます。
any
を使用する場合と異なり、引数と戻り値をnumber型とする1つ目のidentity関数と同様に、
精密(型情報を失わない)であるとも言えるでしょう。
ジェネリックな関数に書き換えたので、2つの方法でこれを呼び出すことができます。 1つ目は型の引数を含め、全ての引数を関数へ渡す方法です。
let output = identity<string>("myString"); // outputの型は'string'になります
ここでは、関数呼び出しの引数の1つとして、T
がstring
であることを明示的に設定しています。
この引数は()
ではなく<>
で囲って示しています。
2つ目の方法は、おそらく最も一般的なものでもあるでしょう。
ここでは型引数の推論を使用し、コンパイラが渡した引数の型を元に、
T
の値を自動的に設定してくれることを期待します。
let output = identity("myString"); // // outputの型は'string'になります
<>
の括弧内に型を明確にして渡す必要が無いことに注目してください。
コンパイラは"myString"の値だけを確認して、T
にその型を設定します。
型の引数の推論がコードを読みやすく短いものに保つ一方で、 より複雑なコードでコンパイラが型の推論に失敗するようなケースでは、 最初の例でそうしたように、型の引数を明確にして渡す必要があるかもしれません。
ジェネリクスの型変数の使用
ジェネリクスの使用を始めるにあたり、identity
のようなジェネリクスの関数を作成する際に、
関数の本文内で、適切にジェネリクス指定した型の引数が使用できる事を、コンパイラに強制させられることになるでしょう。
これでは実際には、引数をまるでany
型(及び全ての型)であるかのように取り扱わなければいけません。
先程のidentity
を例に考えてみましょう。
function identity<T>(arg: T): T {
return arg;
}
仮に、引数arg
のlength
を、コンソールにログ出力したい場合はどうでしょう?
つい、このように書いてしまうかもしれません。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // エラー: Tは.lengthを持ちません
return arg;
}
このようにすると、コンパイラはarg
が.length
を持つことがどこにも説明されていないとして、
arg
が.length
メンバを使用している事に対してエラーを発生させます。
先程、これらの型変数はany
と全ての型の代理であると説明したことを思い出してください。
誰かがこの関数を使用して、.length
を持たない数値(number)を渡すことが出来てしまうということです。
この関数で直接T
を使用するのではなく、T
の配列で動作させることを伝えてみましょう。
配列として動作させるのであれば、.length
メンバは使用可能なはずです。
他の型の配列を作成するように、これを説明することが可能です。
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // 配列は.lengthを持つため、エラーにはなりません
return arg;
}
loggingIdentityの関数は、「ジェネリクスの関数loggingIdentity
は型引数T
を受け取り、
引数arg
はT
の配列であり、T
の配列を返します」と読むことが出来ます。
もし、数値(number)の配列が渡された場合は、T
にはnumber
が紐付けられ、
戻り値として数値の配列が返されることになります。
これにより、ジェネリクスの型変数T
を全ての型として取り扱うのではなく、型の1つとして使用でき、
より柔軟に書けるようになります。
また、次のように書くことも可能です。
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // 配列は.lengthを持つため、エラーにはなりません
return arg;
}
もしかしたら、あなたは既に別の言語でのこの型の書き方に慣れているかもしれません。
次のセクションでは、Array<T>
のような、自身のジェネリクス型の作り方について説明します。
ジェネリクスの型
前のセクションでは、特定の型に囚われないで動作するジェネリクスのidentity
関数を作成しました。
このセクションでは、関数それ自身の型とジェネリクスのインターフェースを作成方法について説明します。
ジェネリクスの関数の型は、それらの非ジェネリクス関数とほとんど同じです。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
また、型変数の数と型変数の並び方が同じであれば、 ジェネリクスの型を異なる名前にすることも可能です。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
また、オブジェクトリテラル型の呼び出しとして、ジェネリクスの型を書くことも可能です。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
次に、初めてのジェネリクスのインターフェースを書いてみましょう。 先の例のオブジェクトリテラルを、インターフェースにしてみます。
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
同様の例として、ジェネリクスのパラメーターを、インターフェース全体のパラメーターに移すことも可能です。
これは、ジェネリクスする型が何なのかを分かりやすくしてくれます
(例: Dictionalry
ではなく、Dictionary<string>
)。
型パラメーターが、インターフェースの他の全てのメンバで可視化されます。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
これらの例の違いが僅かであることに注意してください。 ジェネリクスの関数を表す代わりに、非ジェネリクスの関数をジェネリクスの型の一部としています。
GenericIdentityFn
を使用する場合、それに対応した型を指定し(ここではnumber)、
下地とする呼び出し関数を固定する必要もあります。
型パラメーターを直接呼び出しシグネチャ上に配置するべきタイミング、
またインターフェース自身にそれを配置するべきタイミングを理解することは、ジェネリクスの型を表す手助けになるでしょう。
ジェネリクスのインターフェース(interface)に加え、ジェネリクスのクラス(class)を作成することも可能です。 ジェネリクスのenumと名前空間(namespace)の作成は出来ないことに注意してください。
ジェネリクスのクラス
ジェネリクスのクラスは、ジェネリクスのインターフェースに似た形状をしています。
ジェネリクスのクラスは、ジェネリクスの型パラメーターのリストを、クラスの名前の後ろの括弧<>
内に持ちます。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
これは文字通りGenericNumber
クラスを使用しているに過ぎませんが、
あなたは、数値(number)型だけの使用に制限されているわけではないと、気付いたかもしれません。
これには、文字列または複雑なオブジェクトでさえも、代わりに使用することが可能です。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
インターフェースのように、クラス上の型パラメーターを指定することで、 クラスのプロパティ全てが、それと同じ型で動作することを保証してくれます。
クラスのセクションで、 クラスには静的側とインスタンス側の、2つの型の側面があることを説明しました。 ジェネリクスなクラスでは、静的側ではなくインスタンス側のみが有効であり、 そのためクラスで使用する際には、静的メンバはクラスの型パラメーターを使用することはできません。
ジェネリクスの制約
先程の例を覚えていれば、時には数種類の型がどのような特性なのかを理解した上で、
特定の型に限定されずに動作するジェネリクスの関数を書きたいと考えることがあるでしょう。
loggingIdentity
の例では、arg
の.length
にアクセスできるようにしたいと考えましたが、
コンパイラは全ての型が.length
プロパティを持つとは限らないとして、これに対して警告を発しました。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // エラー: Tは.lengthを持ちません
return arg;
}
any
または全ての型で動作させるのではなく、
.length
プロパティを持つ全ての型で動作するように、この関数に制約を掛けたいと思います。
型がこのメンバを持ちさえすれば良いということは、少なくともこのメンバを持つことが必須となります。
これを実現するために、T
が何になれるのかという制約をかける必要があります。
これを行うために、この制約を表すインターフェースを作成します。
単一の.length
プロパティを持つインターフェースを作成し、
このインターフェースとextends
を使用することで制約を表現します。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // .lengthプロパティを持つことが保証されるため、エラーになりません
return arg;
}
このジェネリクスの関数に制約が掛けられたため、もはやany及び全ての型で動作しません。
loggingIdentity(3); // エラー、numberは.lengthプロパティを持ちません
関数に渡す値の型は、必要とされるプロパティを全て持つ必要があります。
loggingIdentity({length: 10, value: 3});
ジェンリクスの制約での型パラメーターの使用
他の型のパラメーターによって制約がかけられた型のパラメーターを宣言することが可能です。
例えば、2つのオブジェクトを受け取り、片方からもう一方へプロパティをコピーしたいとします。
誤ってsource
に存在しない余計なプロパティが書かれないことを保証するために、
2つの型の間に制約を設けます。
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = source[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 }); // OK
copyFields(x, { Q: 90 }); // エラー、プロパティQは'x'の中で宣言されていません。
ジェネリクスでクラスの型を使用
TypeScriptでジェネリクスを使用してファクトリーを作成する際に、 コンストラクタ関数を使用してクラスの型を参照することが必要になります。 下記はその一例となります。
function create<T>(c: {new(): T; }): T {
return new c();
}
より進んだ例として、参照のためにprototypeプロパティを使用し、 コンストラクタ関数とクラス型のインスタンス側の関係に制約をかけます。
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function findKeeper<A extends Animal, K> (a: {new(): A;
prototype: {keeper: K}}): K {
return a.prototype.keeper;
}
findKeeper(Lion).nametag; // typechecks!
© https://github.com/Microsoft/TypeScript-Handbook
このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- ドキュメントの情報が古い可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。