このページは廃止されました

このハンドブックのページは置き換えられました。新しいページに移動

ジェネリクス

ソフトウェアエンジニアリングの主要な部分は、明確に定義され、一貫性のある 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<T>(arg: T): T {
return arg;
}
Try

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

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

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

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

ここでは、関数呼び出しの引数の1つとして、Tを明示的にstringに設定します。引数を囲む記号は、()ではなく<>を使用することに注意してください。

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

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

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

ジェネリック型変数の使用

ジェネリクスを使い始めると、identityのようなジェネリック関数を作成するときに、コンパイラが関数本体でジェネリック型のパラメータを正しく使用することを強制することに気付くでしょう。つまり、これらのパラメータがあらゆる型になり得るものとして扱う必要があるということです。

先ほどのidentity関数を例に取ってみましょう。

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

引数argの長さをコンソールに毎回ログ出力したい場合はどうでしょうか? このように記述したくなるかもしれません。

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

そうすると、コンパイラはarg.lengthメンバーを使用しているというエラーを出力しますが、argにこのメンバーがあるとはどこにも記述していません。前述したように、これらの型変数はあらゆる型を表すため、この関数を使用する人がnumber型を渡す可能性があり、number型には.lengthメンバーがありません。

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

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

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

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

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

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

~省略~ジェネリック型

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

ジェネリック関数の型は、非ジェネリック関数の型と同様に、型パラメータが関数宣言と同様に最初にリストされます。

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

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

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

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

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

これにより、最初のジェネリックインターフェースを記述できます。前の例のオブジェクトリテラルを取り、インターフェースに移動してみましょう。

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

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

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

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

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

~省略~ジェネリッククラス

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

ts
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;
};
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<T>(arg: T): T {
console.log(arg.length);
Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.
return arg;
}
Try

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

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

ts
interface Lengthwise {
length: number;
}
 
function loggingIdentity<T extends Lengthwise>(arg: T): T {
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<T, K extends keyof T>(obj: T, key: K) {
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<T>(c: { new (): T }): T {
return new c();
}
Try

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

ts
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 createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Try

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

このページの貢献者
RCRyan Cavanaugh (51)
OTOrta Therox (19)
DRDaniel Rosenwasser (19)
MHMohamed Hegazy (5)
RCRick Carlino (4)
15+

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