ジェネリクス

ソフトウェアエンジニアリングの主要な部分は、明確に定義され、一貫性のあるAPIを持つだけでなく、再利用可能なコンポーネントを構築することです。 今日のデータと将来のデータの両方で動作できるコンポーネントは、大規模なソフトウェアシステムを構築するための最も柔軟な機能を提供します。

C#やJavaなどの言語では、再利用可能なコンポーネントを作成するための主要なツールの1つは、_ジェネリクス_です。 つまり、単一の型ではなく、さまざまな型で動作できるコンポーネントを作成できることです。 これにより、ユーザーはこれらのコンポーネントを使用して独自の型を使用できます。

ジェネリクスのHello World

最初に、ジェネリクスの「hello world」である恒等関数を作成してみましょう。 恒等関数は、渡されたものをそのまま返す関数です。 これは`echo`コマンドと同様に考えることができます。

ジェネリクスを使用しない場合、恒等関数に特定の型を指定する必要があります

ts
function identity(arg: number): number {
return arg;
}
Try

または、`any`型を使用して恒等関数を記述することもできます

ts
function identity(arg: any): any {
return arg;
}
Try

`any`を使用すると、関数が`arg`の型としてあらゆる型を受け入れるという点で確かにジェネリックですが、実際には関数が値を返したときにその型に関する情報が失われています。数値を渡した場合、得られる情報は、あらゆる型が返される可能性があるということだけです。

代わりに、引数の型をキャプチャする方法が必要です。その型を使用して、返されるものを示すこともできます。ここでは、値ではなく型に対して機能する特別な種類の変数である_型変数_を使用します。

ts
function identity<Type>(arg: Type): Type {
return arg;
}
Try

これで、型変数`Type`が恒等関数に追加されました。この`Type`により、ユーザーが指定した型(例:`number`)をキャプチャできるため、後でその情報を使用できます。ここでは、戻り値の型として再び`Type`を使用しています。検査すると、引数と戻り値の型に同じ型が使用されていることがわかります。これにより、関数の片側で型情報をやり取りし、反対側から出力できます。

このバージョンの`identity`関数は、さまざまな型で動作するため、ジェネリックであると言います。 `any`を使用するのと異なり、引数と戻り値の型に数値を使用した最初の`identity`関数と同じくらい正確です(つまり、情報を失いません)。

ジェネリック恒等関数を記述したら、2つの方法のいずれかで呼び出すことができます。最初の方法は、型引数を含むすべての引数を関数に渡すことです

ts
let output = identity<string>("myString");
let output: string
Try

ここでは、`()`ではなく引数の周りの`<>`を使用して示されるように、`Type`を明示的に`string`に設定して、関数呼び出しの引数の1つとしています。

2番目の方法は、おそらく最も一般的な方法です。ここでは、_型引数推論_を使用します。つまり、コンパイラーが、渡す引数の型に基づいて`Type`の値を自動的に設定するようにします

ts
let output = identity("myString");
let output: string
Try

山かっこ(`<>`)に型を明示的に渡す必要がなかったことに注意してください。コンパイラーは値`"myString"`を見て、`Type`をその型に設定しました。型引数推論は、コードを短く読みやすくするための便利なツールですが、より複雑な例ではコンパイラーが型を推論できない場合があるため、前の例で行ったように型引数を明示的に渡す必要がある場合があります。

ジェネリック型変数の操作

ジェネリクスの使用を開始すると、`identity`のようなジェネリック関数を作成するときに、コンパイラーは、関数の本体でジェネリックに型指定されたパラメーターを正しく使用することを強制することに気付くでしょう。つまり、これらのパラメーターがあらゆる型であるかのように実際に扱うということです。

先ほどの`identity`関数を見てみましょう

ts
function identity<Type>(arg: Type): Type {
return arg;
}
Try

呼び出しごとに引数`arg`の長さをコンソールに記録したい場合はどうでしょうか。これを書きたくなるかもしれません

ts
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
Property 'length' does not exist on type 'Type'.2339Property 'length' does not exist on type 'Type'.
return arg;
}
Try

これを行うと、コンパイラーは`arg`の`.length`メンバーを使用しているというエラーを出力しますが、`arg`にこのメンバーがあると宣言した場所はありません。前述のように、これらの型変数はあらゆる型を表すため、この関数を使用する人が代わりに`.length`メンバーを持たない`number`を渡した可能性があります。

この関数は、`Type`ではなく`Type`の配列で動作するように意図されていたとしましょう。配列を操作しているので、`.length`メンバーを使用できるはずです。他の型の配列を作成する場合と同様に、これを記述できます

ts
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
Try

loggingIdentity の型は、「ジェネリック関数 loggingIdentity は型パラメータ Type と、Type 型の配列である引数 arg を受け取り、Type 型の配列を返す」と読むことができます。数値の配列を渡すと、Typenumber にバインドされるため、数値の配列が返されます。これにより、ジェネリック型変数 Type を型全体ではなく、操作する型の一部として使用できるようになり、柔軟性が向上します。

サンプルの例は、代わりに次のように書くこともできます。

ts
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
Try

他の言語でこのタイプの型に既に精通しているかもしれません。次のセクションでは、Array<Type> のような独自のジェネリック型を作成する方法について説明します。

ジェネリック型

前のセクションでは、さまざまな型で動作するジェネリックな恒等関数を作成しました。このセクションでは、関数自体の型と、ジェネリックインターフェースを作成する方法について説明します。

ジェネリック関数の型は、関数宣言と同様に、型パラメータが最初にリストされていることを除けば、非ジェネリック関数の型と同じです。

ts
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: <Type>(arg: Type) => Type = identity;
Try

型変数の数と使用方法が一致していれば、型内でジェネリック型パラメータに別の名前を使用することもできます。

ts
function identity<Input>(arg: Input): Input {
return arg;
}
 
let myIdentity: <Input>(arg: Input) => Input = identity;
Try

ジェネリック型を、オブジェクトリテラル型の呼び出しシグネチャとして記述することもできます。

ts
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: { <Type>(arg: Type): Type } = identity;
Try

これは、最初のジェネリックインターフェースを作成することにつながります。前の例のオブジェクトリテラルを取り、インターフェースに移動してみましょう。

ts
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: GenericIdentityFn = identity;
Try

同様の例では、ジェネリックパラメータをインターフェース全体のパラメータに移動したい場合があります。これにより、どの型に対してジェネリックであるかを確認できます(例:単なる Dictionary ではなく Dictionary<string>)。これにより、型パラメータがインターフェースの他のすべてのメンバーに表示されます。

ts
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
return arg;
}
 
let myIdentity: GenericIdentityFn<number> = identity;
Try

この例は、少し異なるものに変更されていることに注意してください。ジェネリック関数を記述する代わりに、ジェネリック型の一部である非ジェネリック関数シグネチャができました。GenericIdentityFn を使用する場合、対応する型引数(ここでは number)も指定する必要があります。これにより、基になる呼び出しシグネチャで使用される内容が事実上ロックされます。型パラメータを呼び出しシグネチャに直接配置する場合と、インターフェース自体に配置する場合を理解することは、型のどの側面がジェネリックであるかを記述するのに役立ちます。

ジェネリックインターフェースに加えて、ジェネリッククラスを作成することもできます。ジェネリックな列挙型と名前空間を作成することはできないことに注意してください。

ジェネリッククラス

ジェネリッククラスは、ジェネリックインターフェースと同様の形状をしています。ジェネリッククラスには、クラス名の後に山かっこ(<>)で囲まれたジェネリック型パラメータリストがあります。

ts
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
 
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
Try

これは GenericNumber クラスのかなり文字通りの使用法ですが、number 型のみを使用するように制限するものがないことに気付いたかもしれません。代わりに string や、さらに複雑なオブジェクトを使用することもできました。

ts
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
 
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
Try

インターフェースと同様に、型パラメータをクラス自体に配置することで、クラスのすべてのプロパティが同じ型で動作することを確認できます。

クラスに関するセクションで説明しているように、クラスには静的側とインスタンス側の 2 つの側面があります。ジェネリッククラスは静的側ではなくインスタンス側に対してのみジェネリックであるため、クラスを操作する場合、静的メンバーはクラスの型パラメータを使用できません。

ジェネリック制約

以前の例を覚えているなら、型のセットに対して動作するジェネリック関数を記述したい場合があります。その型のセットがどのような機能を持っているかについて、ある程度の知識があります。loggingIdentity の例では、arg.length プロパティにアクセスしたかったのですが、コンパイラはすべての型に .length プロパティがあることを証明できなかったため、この仮定を行うことができないという警告が表示されます。

ts
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
Property 'length' does not exist on type 'Type'.2339Property 'length' does not exist on type 'Type'.
return arg;
}
Try

あらゆる型を扱う代わりに、この関数を、.length プロパティも持つあらゆる型で動作するように制約したいと考えています。型がこのメンバーを持っている限り、それを許可しますが、少なくともこのメンバーを持っている必要があります。そのためには、要件を Type にできるものに対する制約としてリストする必要があります。

そのためには、制約を記述するインターフェースを作成します。ここでは、単一の .length プロパティを持つインターフェースを作成し、このインターフェースと extends キーワードを使用して制約を示します。

ts
interface Lengthwise {
length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
Try

ジェネリック関数は制約されるようになったため、あらゆる型に対しては機能しなくなります。

ts
loggingIdentity(3);
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.2345Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
Try

代わりに、必要なプロパティをすべて持つた型の値を渡す必要があります。

ts
loggingIdentity({ length: 10, value: 3 });
Try

ジェネリック制約での型パラメータの使用

別の型パラメータによって制約される型パラメータを宣言できます。たとえば、ここでは、オブジェクトから名前が与えられたプロパティを取得したいと考えています。obj に存在しないプロパティを誤って取得しないように、2 つの型の間に制約を設けます。

ts
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
getProperty(x, "m");
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"'.
Try

ジェネリックでのクラスタイプの使用

TypeScript でジェネリックスを使用してファクトリを作成する場合、クラスタイプをコンストラクタ関数で参照する必要があります。例えば、

ts
function create<Type>(c: { new (): Type }): Type {
return new c();
}
Try

より高度な例では、prototype プロパティを使用して、コンストラクタ関数とクラスタイプのインスタンス側の関係を推測し、制約します。

ts
class BeeKeeper {
hasMask: boolean = true;
}
 
class ZooKeeper {
nametag: string = "Mikle";
}
 
class Animal {
numLegs: number = 4;
}
 
class Bee extends Animal {
numLegs = 6;
keeper: BeeKeeper = new BeeKeeper();
}
 
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
 
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Try

このパターンは、ミックスイン設計パターンを強化するために使用されます。

ジェネリックパラメータのデフォルト

ジェネリック型パラメータのデフォルトを宣言することにより、対応する型引数を指定することをオプションにします。たとえば、新しい HTMLElement を作成する関数です。引数なしで関数を呼び出すと、HTMLDivElement が生成されます。最初の引数として要素を指定して関数を呼び出すと、引数の型の要素が生成されます。必要に応じて、子のリストを渡すこともできます。以前は、関数を次のように定義する必要がありました。

ts
declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;
declare function create<T extends HTMLElement>(element: T): Container<T, T[]>;
declare function create<T extends HTMLElement, U extends HTMLElement>(
element: T,
children: U[]
): Container<T, U[]>;
Try

ジェネリックパラメータのデフォルトを使用すると、次のように減らすことができます。

ts
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
element?: T,
children?: U
): Container<T, U>;
 
const div = create();
const div: Container<HTMLDivElement, HTMLDivElement[]>
 
const p = create(new HTMLParagraphElement());
const p: Container<HTMLParagraphElement, HTMLParagraphElement[]>
Try

ジェネリックパラメータのデフォルトは、次のルールに従います。

  • 型パラメータにデフォルトがある場合、オプションと見なされます。
  • 必須の型パラメータは、オプションの型パラメータの後に続いてはいけません。
  • 型パラメータのデフォルト型は、存在する場合、型パラメータの制約を満たす必要があります。
  • 型引数を指定する場合、必須の型パラメータの型引数のみを指定する必要があります。指定されていない型パラメータは、デフォルトの型に解決されます。
  • デフォルト型が指定されていて、推論が候補を選択できない場合、デフォルト型が推論されます。
  • 既存のクラスまたはインターフェース宣言とマージされるクラスまたはインターフェース宣言は、既存の型パラメータのデフォルトを導入する場合があります。
  • 既存のクラスまたはインターフェース宣言とマージされるクラスまたはインターフェース宣言は、デフォルトを指定する限り、新しい型パラメータを導入する場合があります。

TypeScript のドキュメントはオープンソースプロジェクトです。 プルリクエストを送信して、これらのページの改善にご協力ください ❤

このページの貢献者
OTOrta Therox (26)
NKNavneet Karnani (2)
JBJake Bailey (1)
MMFredX (1)
ZSZack Schuster (1)
6+

最終更新日: 2024年3月21日