Generics(ジェネリクス)とは、型引数を受け取る関数を作る機能になります。
本記事では、TypeScriptによるプログラミングにおいて欠かすことができない機能であるGenericsを解説します。
Generics(ジェネリクス)とは?
Generics(ジェネリクス)とは、型引数を受け取る関数を作る機能になります。(以降ジェネリクス)
ジェネリクスは、TypeScriptによるプログラミングにおいて欠かすことができない機能です。
一般的に、型引数の概念は次のようなtype文と組み合わせて利用するものです。
type User<T> = {
name: T;
}
一方で、ジェネリクスにおいて型引数を持つのは関数やクラスになります。
Generics(ジェネリクス)の基本的な使い方
型引数を持つ関数やクラスなどを宣言する場合、関数・クラス名のあとに<型引数リスト>といった構文を付け足すのが基本形になります。
本記事では、以下のパターンでジェネリクスの使い方を解説します。
- 関数におけるGenerics(ジェネリクス)
- クラスにおけるGenerics(ジェネリクス)の使い方
- インターフェースにおけるGenerics(ジェネリクス)の使い方
それぞれ解説していきます。
関数におけるGenerics(ジェネリクス)の使い方
ジェネリクスは、抽象的な型引数を利用し、宣言以降のコード上で利用された場合に型が確定することになります。
具体例として、類似したコードが存在した場合を考えます。
function sample1(arg: number): number {
return arg;
}
function sample2(arg: string): string {
return arg;
}
sample1(17); //=> 17
sample2("文字列"); //=> 文字列
上記のコードでは、各関数において型を確定させている状態だと理解すればよいです。
上記の関数に対してジェネリクス型を適用したものが以下になります。
function sample<T>(arg: T): T {
return arg;
}
sample<number>(17); //=> 17
sample<string>("文字列"); //=> 文字列
sample("サンプル")
// ※TypeScriptによる型推論にて引数から明示的に型が判断できれば省略可能
純粋な関数とジェネリクス型関数を比較すると、関数内による型定義が明示的か抽象的かによって異なります。
また、ジェネリクス型関数は呼び出しの際に型が決まります。
つまり、抽象的な型引数<T>を定義し利用時に型が確定する関数を作成します。
クラスにおけるGenerics(ジェネリクス)の使い方
ジェネリクス関数と同様に型引数を持たせることでクラスもジェネリクス化できます。
実際に、関数の返り値の型や引数の型として利用できます。
class Test<T> {
item: T;
constructor(item: T) {
this.item = item;
}
getItem(): T {
return this.item;
}
}
let strObj = new Test<string>("文字列");
strObj.getItem(); //=> "文字列"
let numObj = new Klass<number>(17);
numObj.getItem(); //=> 17
インターフェースにおけるGenerics(ジェネリクス)の使い方
こちらも上記のジェネリクス型関数・クラスと同様に、interface(インターフェース)を作成する事が可能です。
interface TestValue<T, U> {
str: T;
value: U;
}
let obj: TestValue<string, number> = { str: "文字列", value: 17}; //=> {key: "文字列", value: 17}
Generics(ジェネリクス)における型推論と制約について
ここまで、関数・クラス・インターフェースでジェネリクス型の使い方を解説しました。
ただ、よりジェネリクスを深く理解するために以下の内容を深堀りします。
- 型引数はどのように推論されるのか
- 型引数に制約を持たせる
それぞれを解説します。
型引数はどのように推論されるのか
ジェネリクス型については、呼び出し時に型推論されると述べてきました。
上記内容を踏まえた上で以下のパターンを考えてみます。
function makeTriple<T>(x: T, y: T, z: T): T[] {
return [x, y, z];
}
const stringTriple = makeTriple("test1", "test2", "test3");
上記は、与えられた3つの値を引数に並べた配列を返す関数になります。
具体例のように、3つの引数に文字列を与え呼び出せば、型引数<T>はstring型と推論されるため、stringTripleはstring[]型になります。
さらに、次のパターンを考えてみます。
function makeTriple<T>(x: T, y: T, z: T): T[] {
return [x, y, z];
}
// エラー: Argument of type 'number' is not assignable to parameter of type 'string'.
const stringTriple = makeTriple("test1", 17, "test3");
上記の場合は、2つ目の引数をnumber型である数値を持たせています。
この場合は、エラー内容としてstring型ではない値が存在するため、コンパイルエラーが発生しています。
つまり、型引数<T>はstring型として推論していることになります。
そのため、ジェネリックス型はなるべく前の引数を用いて型推論していることが分かります。
型引数に制約を持たせる
本記事では、ジェネリックス型引数はどんな型の引数も受け入れてきました。
しかし、引数で受け入れる値を特定の型のみに制限したいケースもあります。
下記の例ではargのnameというプロパティを取得しようとしていますが、全ての型がnameを持つ訳ではないので、コンパイラが警告を出しています。
function getName<S>(arg: S): string {
return arg.name; // Property 'name' does not exist on type 'T'.
}
// argの型は関数宣言時点でnameを持つか不明なためコンパイルエラー
その様な場合、下記の様に書くことで、Sはextendsで指定したtypeを満たす型でなければならないということを指定する事ができます。
type T = {
name: string;
}
function getName<S extends T>(arg: S): string {
return arg.name;
}
getName({ name: "サンプルマン" });
このような操作を部分型関係といい、TypeScriptを理解するにあたって非常に重要な概念になります。
部分型はTypeScriptの型システムにおいて根幹をなす要素の一つと言えます。
具体的に言うと、部分型とは「2つの型の互換性を表す概念」になります。
「型Sが型Tの部分型である」とは、S型の値がT型の値でもあると指します。
ここではオブジェクト型の場合におけるプロパティの包含関係によって部分型関係を説明します。
プロパティの包含関係によって発生する部分型関係は、具体的に2つの条件が満たされれば発生します。
型Sと型Tがオブジェクト型だとして、次の条件が満たされればSがTの部分型になります。
- Tが持つプロパティは全てSにも存在する
- 条件1の各プロパティについて、Sにおけるプロパティの型はTにおけるプロパティの型の部分型(または同じ型)
かなりややこしく感じますが、部分型関係を深く理解することでTypeScriptによる型レベルプログラミングが確実に上達します。
コメント