ソフトウェアエンジニアリングの主要な部分は、明確に定義され、一貫性のある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 <Type >(arg :Type ):Type {returnarg ;}
これで、型変数`Type`が恒等関数に追加されました。この`Type`により、ユーザーが指定した型(例:`number`)をキャプチャできるため、後でその情報を使用できます。ここでは、戻り値の型として再び`Type`を使用しています。検査すると、引数と戻り値の型に同じ型が使用されていることがわかります。これにより、関数の片側で型情報をやり取りし、反対側から出力できます。
このバージョンの`identity`関数は、さまざまな型で動作するため、ジェネリックであると言います。 `any`を使用するのと異なり、引数と戻り値の型に数値を使用した最初の`identity`関数と同じくらい正確です(つまり、情報を失いません)。
ジェネリック恒等関数を記述したら、2つの方法のいずれかで呼び出すことができます。最初の方法は、型引数を含むすべての引数を関数に渡すことです
tsTry
letoutput =identity <string>("myString");
ここでは、`()`ではなく引数の周りの`<>`を使用して示されるように、`Type`を明示的に`string`に設定して、関数呼び出しの引数の1つとしています。
2番目の方法は、おそらく最も一般的な方法です。ここでは、_型引数推論_を使用します。つまり、コンパイラーが、渡す引数の型に基づいて`Type`の値を自動的に設定するようにします
tsTry
letoutput =identity ("myString");
山かっこ(`<>`)に型を明示的に渡す必要がなかったことに注意してください。コンパイラーは値`"myString"`を見て、`Type`をその型に設定しました。型引数推論は、コードを短く読みやすくするための便利なツールですが、より複雑な例ではコンパイラーが型を推論できない場合があるため、前の例で行ったように型引数を明示的に渡す必要がある場合があります。
ジェネリック型変数の操作
ジェネリクスの使用を開始すると、`identity`のようなジェネリック関数を作成するときに、コンパイラーは、関数の本体でジェネリックに型指定されたパラメーターを正しく使用することを強制することに気付くでしょう。つまり、これらのパラメーターがあらゆる型であるかのように実際に扱うということです。
先ほどの`identity`関数を見てみましょう
tsTry
functionidentity <Type >(arg :Type ):Type {returnarg ;}
呼び出しごとに引数`arg`の長さをコンソールに記録したい場合はどうでしょうか。これを書きたくなるかもしれません
tsTry
functionloggingIdentity <Type >(arg :Type ):Type {Property 'length' does not exist on type 'Type'.2339Property 'length' does not exist on type 'Type'.console .log (arg .); length returnarg ;}
これを行うと、コンパイラーは`arg`の`.length`メンバーを使用しているというエラーを出力しますが、`arg`にこのメンバーがあると宣言した場所はありません。前述のように、これらの型変数はあらゆる型を表すため、この関数を使用する人が代わりに`.length`メンバーを持たない`number`を渡した可能性があります。
この関数は、`Type`ではなく`Type`の配列で動作するように意図されていたとしましょう。配列を操作しているので、`.length`メンバーを使用できるはずです。他の型の配列を作成する場合と同様に、これを記述できます
tsTry
functionloggingIdentity <Type >(arg :Type []):Type [] {console .log (arg .length );returnarg ;}
loggingIdentity
の型は、「ジェネリック関数 loggingIdentity
は型パラメータ Type
と、Type
型の配列である引数 arg
を受け取り、Type
型の配列を返す」と読むことができます。数値の配列を渡すと、Type
が number
にバインドされるため、数値の配列が返されます。これにより、ジェネリック型変数 Type
を型全体ではなく、操作する型の一部として使用できるようになり、柔軟性が向上します。
サンプルの例は、代わりに次のように書くこともできます。
tsTry
functionloggingIdentity <Type >(arg :Array <Type >):Array <Type > {console .log (arg .length ); // Array has a .length, so no more errorreturnarg ;}
他の言語でこのタイプの型に既に精通しているかもしれません。次のセクションでは、Array<Type>
のような独自のジェネリック型を作成する方法について説明します。
ジェネリック型
前のセクションでは、さまざまな型で動作するジェネリックな恒等関数を作成しました。このセクションでは、関数自体の型と、ジェネリックインターフェースを作成する方法について説明します。
ジェネリック関数の型は、関数宣言と同様に、型パラメータが最初にリストされていることを除けば、非ジェネリック関数の型と同じです。
tsTry
functionidentity <Type >(arg :Type ):Type {returnarg ;}letmyIdentity : <Type >(arg :Type ) =>Type =identity ;
型変数の数と使用方法が一致していれば、型内でジェネリック型パラメータに別の名前を使用することもできます。
tsTry
functionidentity <Input >(arg :Input ):Input {returnarg ;}letmyIdentity : <Input >(arg :Input ) =>Input =identity ;
ジェネリック型を、オブジェクトリテラル型の呼び出しシグネチャとして記述することもできます。
tsTry
functionidentity <Type >(arg :Type ):Type {returnarg ;}letmyIdentity : { <Type >(arg :Type ):Type } =identity ;
これは、最初のジェネリックインターフェースを作成することにつながります。前の例のオブジェクトリテラルを取り、インターフェースに移動してみましょう。
tsTry
interfaceGenericIdentityFn {<Type >(arg :Type ):Type ;}functionidentity <Type >(arg :Type ):Type {returnarg ;}letmyIdentity :GenericIdentityFn =identity ;
同様の例では、ジェネリックパラメータをインターフェース全体のパラメータに移動したい場合があります。これにより、どの型に対してジェネリックであるかを確認できます(例:単なる Dictionary
ではなく Dictionary<string>
)。これにより、型パラメータがインターフェースの他のすべてのメンバーに表示されます。
tsTry
interfaceGenericIdentityFn <Type > {(arg :Type ):Type ;}functionidentity <Type >(arg :Type ):Type {returnarg ;}letmyIdentity :GenericIdentityFn <number> =identity ;
この例は、少し異なるものに変更されていることに注意してください。ジェネリック関数を記述する代わりに、ジェネリック型の一部である非ジェネリック関数シグネチャができました。GenericIdentityFn
を使用する場合、対応する型引数(ここでは number
)も指定する必要があります。これにより、基になる呼び出しシグネチャで使用される内容が事実上ロックされます。型パラメータを呼び出しシグネチャに直接配置する場合と、インターフェース自体に配置する場合を理解することは、型のどの側面がジェネリックであるかを記述するのに役立ちます。
ジェネリックインターフェースに加えて、ジェネリッククラスを作成することもできます。ジェネリックな列挙型と名前空間を作成することはできないことに注意してください。
ジェネリッククラス
ジェネリッククラスは、ジェネリックインターフェースと同様の形状をしています。ジェネリッククラスには、クラス名の後に山かっこ(<>
)で囲まれたジェネリック型パラメータリストがあります。
tsTry
classGenericNumber <NumType > {zeroValue :NumType ;add : (x :NumType ,y :NumType ) =>NumType ;}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 <Type >(arg :Type ):Type {Property 'length' does not exist on type 'Type'.2339Property 'length' does not exist on type 'Type'.console .log (arg .); length returnarg ;}
あらゆる型を扱う代わりに、この関数を、.length
プロパティも持つあらゆる型で動作するように制約したいと考えています。型がこのメンバーを持っている限り、それを許可しますが、少なくともこのメンバーを持っている必要があります。そのためには、要件を Type
にできるものに対する制約としてリストする必要があります。
そのためには、制約を記述するインターフェースを作成します。ここでは、単一の .length
プロパティを持つインターフェースを作成し、このインターフェースと extends
キーワードを使用して制約を示します。
tsTry
interfaceLengthwise {length : number;}functionloggingIdentity <Type extendsLengthwise >(arg :Type ):Type {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 <Type ,Key extends keyofType >(obj :Type ,key :Key ) {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 <Type >(c : { new ():Type }):Type {return newc ();}
より高度な例では、prototype プロパティを使用して、コンストラクタ関数とクラスタイプのインスタンス側の関係を推測し、制約します。
tsTry
classBeeKeeper {hasMask : boolean = true;}classZooKeeper {nametag : string = "Mikle";}classAnimal {numLegs : number = 4;}classBee extendsAnimal {numLegs = 6;keeper :BeeKeeper = newBeeKeeper ();}classLion extendsAnimal {keeper :ZooKeeper = newZooKeeper ();}functioncreateInstance <A extendsAnimal >(c : new () =>A ):A {return newc ();}createInstance (Lion ).keeper .nametag ;createInstance (Bee ).keeper .hasMask ;
このパターンは、ミックスイン設計パターンを強化するために使用されます。
ジェネリックパラメータのデフォルト
ジェネリック型パラメータのデフォルトを宣言することにより、対応する型引数を指定することをオプションにします。たとえば、新しい HTMLElement
を作成する関数です。引数なしで関数を呼び出すと、HTMLDivElement
が生成されます。最初の引数として要素を指定して関数を呼び出すと、引数の型の要素が生成されます。必要に応じて、子のリストを渡すこともできます。以前は、関数を次のように定義する必要がありました。
tsTry
declare functioncreate ():Container <HTMLDivElement ,HTMLDivElement []>;declare functioncreate <T extendsHTMLElement >(element :T ):Container <T ,T []>;declare functioncreate <T extendsHTMLElement ,U extendsHTMLElement >(element :T ,children :U []):Container <T ,U []>;
ジェネリックパラメータのデフォルトを使用すると、次のように減らすことができます。
tsTry
declare functioncreate <T extendsHTMLElement =HTMLDivElement ,U =T []>(element ?:T ,children ?:U ):Container <T ,U >;constdiv =create ();constp =create (newHTMLParagraphElement ());
ジェネリックパラメータのデフォルトは、次のルールに従います。
- 型パラメータにデフォルトがある場合、オプションと見なされます。
- 必須の型パラメータは、オプションの型パラメータの後に続いてはいけません。
- 型パラメータのデフォルト型は、存在する場合、型パラメータの制約を満たす必要があります。
- 型引数を指定する場合、必須の型パラメータの型引数のみを指定する必要があります。指定されていない型パラメータは、デフォルトの型に解決されます。
- デフォルト型が指定されていて、推論が候補を選択できない場合、デフォルト型が推論されます。
- 既存のクラスまたはインターフェース宣言とマージされるクラスまたはインターフェース宣言は、既存の型パラメータのデフォルトを導入する場合があります。
- 既存のクラスまたはインターフェース宣言とマージされるクラスまたはインターフェース宣言は、デフォルトを指定する限り、新しい型パラメータを導入する場合があります。