こんにちは、フロントエンドエンジニアのまさにょんです。
今回は、TypeScriptにおけるGenerics(ジェネリクス)の使い方について解説して行きます。
目次
Generics(ジェネリクス)とは何か?
Generics(ジェネリクス)を使用すると「型の安全性とコードの共通化を両立する」ことができます。
迷えるTypeScript-Beginnerのバイブルである『サバイバルTypeScript』では次のように記述されています。
コードの共通化と型の安全性の両方を達成するにはどうしたらいいのでしょうか?
ここで、役に立つのがジェネリクスです。
ジェネリクスの発想は実はとてもシンプルで、「型も変数のように扱えるようにする」というものです。
引用元:サバイバルTypeScript: ジェネリクス (generics)
この「型も変数のように扱えるようにする」と言うのが、Genericsの特徴です。
Genericsとは、「汎用」と言う意味の英単語です。
プログラミングにおけるGenericsは「型とそれを使用した処理を汎用的に使い回せるようにしたもの」だと言えます。
このように処理などを「汎用化」して、使い回せるようにすると、同じような記述をGenericsにまとめることができて、Code量を削減することができるというメリットがあります。
TypeScript以外にも、C#やJavaといった言語もGenericsを搭載しています。
TypeScriptにおけるGenericsは「抽象的」な「型引数」を使用して、「実際に利用されるまで型が確定しない」クラス・関数・インターフェイスを実現する為に使用されます。
つまり、「抽象化」することでInterface(窓口)としては「汎用化」して、「具体的な型」がParameterとして渡ってくることで、初めて中身の処理も「具体化」されるといった特徴を持っています。
それでは、Sample-Codeを観て行きましょう。
// [ Generics の簡単な具体例(関数編) ]
// number型-専用-Function
function test(arg: number): number {
return arg;
}
// string型-専用-Function
function test2(arg: string): string {
return arg;
}
test(1); //=> 1
test2("文字列"); //=> 文字列
// 1. 上記2つのFunctionは、扱うデータの「型」が違うだけで構造的には一緒です。
// 2. このような構造が共通の処理は、Genericsにしましょう!
// 3. Generics Ver.Function
function GenericsTest<T>(arg: T): T {
return arg;
}
// 4. 呼び出す時に、「型引数」に「具体的」な型を渡す(型を決定する)
GenericsTest<number>(1); // 1
GenericsTest<string>("ももちゃん"); // ももちゃん
// 5. Genericsでも型推論ができるので、引数から型が明示的にわかる場合は省略が可能
GenericsTest("白桃"); // 白桃
// < まとめ >
// Genericsでは抽象的な型引数<T>を関数に与え、実際に利用されるまで型が確定しない関数を作成しています。
TypeScriptでのGenerics-基本構文
ここで一度、TypeScriptにおけるGenericsの基本構文を確認しましょう。
Genericsでは、Function, Class, interfaceなど のObject-構造に対して、< 型引数 > の形で「抽象化したParameter」を設定することができます。
Object<型引数> { 処理を記述 }
Function, Class, interfaceなど Object-構造に対して、<型引数>を設定して、処理内で使用することができる。
複数の型引数を定義するPattern
Genericsでは、複数の型引数を使用することも可能です。
抽象的な型引数の設定なので、慣習的にT(Type), U(Use), P(Param)などの大文字アルファベットを使用することが多いようです。
// [ 複数の型引数を定義する ]
function MultiGenerics<T, U, P>(arg1:T, arg2: U, arg3: P): P {
return arg3;
}
//※ Genericsでも型推論ができるので、引数から型が明示的にわかる場合は省略が可能
MultiGenerics({robotama: 'ロボ玉'}, true, 'ロボ玉試作1号機'); // ロボ玉試作1号機
Genericsの簡単な具体例(クラス編)
Generic関数の様に < 型引数 > を渡す事で、クラスもGenerics化する事が可能です。
// [ Genericsの簡単な具体例(クラス編) ]
// 1. 型引数Tはメソッドの返り値の型や、引数の型として、クラスを通して使用されている事が見てとれます。
class GenericsClass<T> {
item: T;
constructor(item: T) {
this.item = item;
}
getItem(): T {
return this.item;
}
}
const strObj = new GenericsClass<string>("Generics-Type-ロボ玉");
console.log(strObj.getItem()); // Generics-Type-ロボ玉
const numObj = new GenericsClass<number>(12);
console.log(numObj.getItem()); // 12
Genericsの簡単な具体例(インターフェイス編)
TypeScriptのInterfaceでもGenericを使用する事が可能です。
// [ Genericsの簡単な具体例(インターフェイス編) ]
interface KeyValue<T, U> {
key: T;
value: U;
}
const obj: KeyValue<string, number> = { key: "ももちゃん", value: true }
console.log(obj); // {key: "ももちゃん", value: true}
Genericsの型引数に制約をかける
ここまで紹介したGenericの型引数はどんな型の引数も受け入れてきました。
しかし、「引数で受け入れる値を特定の型のみに制限したい場合」もあります。
例えば下記の例ではargのnameというプロパティを取得しようとしていますが、全ての型がnameを持つ訳ではないので、コンパイラが警告を出しています。
function getName<T>(arg: T): string {
return arg.name; // Error-Occur: Property 'name' does not exist on type 'T'.
}
// argの型はこの時点でnameを持つか不明なので、コンパイラは警告を出す。
次の様に書くことで「型引数はextendsで指定したインタフェースを満たす型でなければならない」ということを指定する事ができます。
「 型引数 extends 実体のある型 」
// [ 型引数に制約を付ける ]
interface argTypes {
name: string;
}
// 1.「 型引数 extends 実体のある型 」=> 「型引数に制約を入れる」ことができる!
function GenericsGetName<T extends argTypes>(arg: T): string {
return arg.name;
}
GenericsGetName({ name: "ロボ・ロボ玉" });
以上、Genericsの解説でした。
Genericsを活用して、Code量を最適化して行きましょう!
TypeScript書籍
参考・引用
1. 【TypeScript】Generics(ジェネリクス)を理解する
2. 仕事ですぐに使えるTypeScript: ジェネリクス
3. サバイバルTypeScript: ジェネリクス (generics)