ソフトウェアエンジニアリングの主要な部分は、明確に定義され、一貫性のある API を持つだけでなく、再利用可能なコンポーネントを構築することです。 今日のデータと将来のデータの両方で動作できるコンポーネントは、大規模なソフトウェアシステムを構築するための最も柔軟な機能を提供します。
C# や Java などの言語では、再利用可能なコンポーネントを作成するための主要なツールの 1 つは *ジェネリクス* です。 つまり、単一の型ではなく、さまざまな型で動作できるコンポーネントを作成できることです。 これにより、ユーザーはこれらのコンポーネントを使用して独自の型を使用できます。
ジェネリクスの Hello World
まず、ジェネリクスの「hello world」である恒等関数を作成してみましょう。 恒等関数は、渡されたものをそのまま返す関数です。 これは echo
コマンドと同様に考えることができます。
ジェネリクスがない場合、恒等関数に特定の型を指定する必要があります
tsTry
functionidentity (arg : number): number {returnarg ;}
または、`any` 型を使用して恒等関数を記述できます
tsTry
functionidentity (arg : any): any {returnarg ;}
any
を使用すると、関数が `arg` の型としてあらゆる型を受け入れるという意味で確かにジェネリックになりますが、実際には関数が値を返すときにその型に関する情報が失われます。数値を渡した場合、得られる情報は任意の型が返される可能性があるということだけです。
代わりに、引数の型をキャプチャする方法が必要です。その方法では、返されるものを表すためにも使用できます。ここでは、値ではなく型で動作する特別な種類の変数である *型変数* を使用します。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}
これで、型変数 `T` が恒等関数に追加されました。この `T` を使用すると、ユーザーが指定した型 (例: `number`) をキャプチャできるため、その情報を後で 使用できます。ここでは、戻り値の型として `T` を再び使用します。検査すると、引数と戻り値の型に同じ型が使用されていることがわかります。これにより、関数の片側でその型情報をやり取りし、反対側から出力できます。
このバージョンの `identity` 関数は、さまざまな型で動作するため、ジェネリックであると言います。 `any` を使用する場合とは異なり、引数と戻り値の型に数値を使用した最初の `identity` 関数と同じくらい正確です (つまり、情報を失いません)。
ジェネリック恒等関数を記述したら、2 つの方法のいずれかで呼び出すことができます。最初の方法は、型引数を含むすべての引数を関数に渡す方法です
tsTry
letoutput =identity <string>("myString");
ここでは、関数呼び出しの引数の1つとして、T
を明示的にstring
に設定します。引数を囲む記号は、()
ではなく<>
を使用することに注意してください。
2つ目の方法は、おそらく最も一般的な方法です。ここでは、*型引数推論*を使用します。つまり、コンパイラが渡す引数の型に基づいて、T
の値を自動的に設定するようにします。
tsTry
letoutput =identity ("myString");
山括弧(<>
)内に型を明示的に渡す必要がないことに注意してください。コンパイラは値"myString"
を見て、T
をその型に設定します。型引数推論は、コードを短く、読みやすくするための便利なツールですが、より複雑な例では、コンパイラが型を推論できない場合、前の例で行ったように、型引数を明示的に渡す必要がある場合があります。
ジェネリック型変数の使用
ジェネリクスを使い始めると、identity
のようなジェネリック関数を作成するときに、コンパイラが関数本体でジェネリック型のパラメータを正しく使用することを強制することに気付くでしょう。つまり、これらのパラメータがあらゆる型になり得るものとして扱う必要があるということです。
先ほどのidentity
関数を例に取ってみましょう。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}
引数arg
の長さをコンソールに毎回ログ出力したい場合はどうでしょうか? このように記述したくなるかもしれません。
tsTry
functionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
そうすると、コンパイラはarg
の.length
メンバーを使用しているというエラーを出力しますが、arg
にこのメンバーがあるとはどこにも記述していません。前述したように、これらの型変数はあらゆる型を表すため、この関数を使用する人がnumber
型を渡す可能性があり、number
型には.length
メンバーがありません。
この関数をT
ではなく、T
の配列に対して動作させることを意図していたとしましょう。配列を扱うため、.length
メンバーは使用可能であるはずです。他の型の配列を作成する場合と同様に、これを記述できます。
tsTry
functionloggingIdentity <T >(arg :T []):T [] {console .log (arg .length );returnarg ;}
loggingIdentity
の型は、「ジェネリック関数loggingIdentity
は、型パラメータT
と、T
の配列である引数arg
を取り、T
の配列を返す」と読むことができます。数値の配列を渡すと、T
がnumber
にバインドされるため、数値の配列が返されます。これにより、ジェネリック型変数T
を型全体ではなく、扱う型の一部として使用できるようになり、柔軟性が向上します。
代わりに、サンプル例をこのように書くこともできます。
tsTry
functionloggingIdentity <T >(arg :Array <T >):Array <T > {console .log (arg .length ); // Array has a .length, so no more errorreturnarg ;}
他の言語でこの型の記述方法を既に知っているかもしれません。次のセクションでは、Array<T>
のような独自のジェネリック型を作成する方法について説明します。
~省略~ジェネリック型
前のセクションでは、さまざまな型で動作するジェネリックな恒等関数を作成しました。このセクションでは、関数自体の型と、ジェネリックインターフェースを作成する方法について説明します。
ジェネリック関数の型は、非ジェネリック関数の型と同様に、型パラメータが関数宣言と同様に最初にリストされます。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <T >(arg :T ) =>T =identity ;
型変数の数と型変数の使用方法が一致していれば、型でジェネリック型パラメータに別の名前を使用することもできます。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <U >(arg :U ) =>U =identity ;
ジェネリック型をオブジェクトリテラル型の呼び出しシグネチャとして記述することもできます。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : { <T >(arg :T ):T } =identity ;
これにより、最初のジェネリックインターフェースを記述できます。前の例のオブジェクトリテラルを取り、インターフェースに移動してみましょう。
tsTry
interfaceGenericIdentityFn {<T >(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn =identity ;
同様の例では、ジェネリックパラメータをインターフェース全体のパラメータに移動したい場合があります。これにより、どの型をジェネリック化しているかを確認できます(例:Dictionary
ではなくDictionary<string>
)。これにより、型パラメータがインターフェースの他のすべてのメンバーに表示されます。
tsTry
interfaceGenericIdentityFn <T > {(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn <number> =identity ;
例が少し異なるものに変更されていることに注意してください。ジェネリック関数を記述する代わりに、ジェネリック型の一部である非ジェネリック関数シグネチャができました。GenericIdentityFn
を使用する場合、対応する型引数(ここでは:number
)も指定する必要があり、基になる呼び出しシグネチャで使用される内容が事実上固定されます。型パラメータを呼び出しシグネチャに直接配置する場合と、インターフェース自体に配置する場合を理解することは、型のどの側面がジェネリックであるかを記述するのに役立ちます。
ジェネリックインターフェースに加えて、ジェネリッククラスを作成することもできます。ジェネリックな列挙型と名前空間を作成することはできません。
~省略~ジェネリッククラス
ジェネリッククラスは、ジェネリックインターフェースと同様の形状をしています。ジェネリッククラスは、クラス名の後に山括弧(<>
)で囲まれたジェネリック型パラメータリストを持ちます。
tsTry
classGenericNumber <T > {zeroValue :T ;add : (x :T ,y :T ) =>T ;}letmyGenericNumber = newGenericNumber <number>();myGenericNumber .zeroValue = 0;myGenericNumber .add = function (x ,y ) {returnx +y ;};
これはGenericNumber
クラスをかなり文字通り使用した例ですが、number
型のみを使用するように制限するものがないことに気付いたかもしれません。代わりに、string
、あるいはもっと複雑なオブジェクトを使用することもできました。
tsTry
letstringNumeric = newGenericNumber <string>();stringNumeric .zeroValue = "";stringNumeric .add = function (x ,y ) {returnx +y ;};console .log (stringNumeric .add (stringNumeric .zeroValue , "test"));
インターフェースと同様に、型パラメータをクラス自体に配置することで、クラスのすべてのプロパティが同じ型で動作することを確認できます。
クラスに関するセクションで説明したように、クラスには静的側とインスタンス側の2つの側面があります。ジェネリッククラスは静的側ではなくインスタンス側のみでジェネリックであるため、クラスを扱う場合、静的メンバーはクラスの型パラメータを使用できません。
~省略~ジェネリック制約
前の例を覚えているなら、型の集合が持つ機能についてある程度の知識がある場合に、その型の集合で動作するジェネリック関数を記述したい場合があります。loggingIdentity
の例では、arg
の.length
プロパティにアクセスしたかったのですが、コンパイラはすべての型に.length
プロパティがあることを証明できなかったため、この仮定を行うことができないという警告が表示されます。
tsTry
functionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
あらゆる型を扱う代わりに、この関数を.length
プロパティを持つあらゆる型で動作するように制約したいと考えています。型がこのメンバーを持っている限り、それを許可しますが、少なくともこのメンバーを持っている必要があります。そのためには、要件をTが何であるかについての制約としてリストする必要があります。
そのためには、制約を記述するインターフェースを作成します。ここでは、単一の.length
プロパティを持つインターフェースを作成し、このインターフェースとextends
キーワードを使用して制約を示します。
tsTry
interfaceLengthwise {length : number;}functionloggingIdentity <T extendsLengthwise >(arg :T ):T {console .log (arg .length ); // Now we know it has a .length property, so no more errorreturnarg ;}
ジェネリック関数は制約されているため、あらゆる型では動作しなくなりました。
tsTry
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.2345Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.loggingIdentity (3 );
代わりに、必要なプロパティをすべて持つ型の値を渡す必要があります。
tsTry
loggingIdentity ({length : 10,value : 3 });
~省略~ジェネリック制約での型パラメータの使用
別の型パラメータによって制約される型パラメータを宣言できます。たとえば、ここでは、オブジェクトから名前を指定してプロパティを取得したいと考えています。obj
に存在しないプロパティを誤って取得しないように、2つの型間に制約を設けます。
tsTry
functiongetProperty <T ,K extends keyofT >(obj :T ,key :K ) {returnobj [key ];}letx = {a : 1,b : 2,c : 3,d : 4 };getProperty (x , "a");Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.2345Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.getProperty (x ,"m" );
~省略~ジェネリクスでのクラスタイプの使用
TypeScriptでジェネリクスを使用してファクトリを作成する場合、クラス型をそのコンストラクタ関数によって参照する必要があります。例えば、
tsTry
functioncreate <T >(c : { new ():T }):T {return newc ();}
より高度な例では、prototypeプロパティを使用して、コンストラクタ関数とクラス型のインスタンス側の関係を推論し、制約します。
tsTry
classBeeKeeper {hasMask : boolean;}classZooKeeper {nametag : string;}classAnimal {numLegs : number;}classBee extendsAnimal {keeper :BeeKeeper ;}classLion extendsAnimal {keeper :ZooKeeper ;}functioncreateInstance <A extendsAnimal >(c : new () =>A ):A {return newc ();}createInstance (Lion ).keeper .nametag ;createInstance (Bee ).keeper .hasMask ;