【TypeScript】ジェネリクス(Generics)入門。ジェネリクスの種類と利用例

JavaScript

ジェネリクス(Generics)について

プログラミングにおけるジェネリクス(Generics)とは、異なる型を同じコードで処理できるようにする手法のことをいいます。
ジェネリクスは型を抽象化した汎用的なロジックを実装する際に利用します。

型を<>の中に記述することでジェネリクスを表現できます。ジェネリクスで利用する型は大文字のアルファベットを利用して<T>のように記述するのが慣習となっています。

補足: 『ジェネリクス』と『ジェネリック』の呼び方の違いについて

ネットにはジェネリクスとジェネリックの表記が混在しているので呼び方について整理をします。

generics(ジェネリクス)は名詞、generic(ジェネリック)は形容詞です。ですので、本記事では品詞のルールに基づき、手法のことを『ジェネリクス』、ジェネリクスを利用した実装の具体例を『ジェネリックxxx』と紹介します。

ジェネリクスの種類について

ジェネリクスを取り入れた実装例について紹介します。今回紹介する実装例は以下の通りです。

  • ジェネリックタイプ(Generic Types、総称型)
  • ジェネリックメソッド(ジェネリック関数)
  • ジェネリッククラス

ジェネリックタイプ(Generic Types、総称型)

ジェネリックタイプ(Generic Types、総称型)とは、ジェネリクスの手法を取り入れた型、つまり汎用的な型を表現するものです。

例えば、itemというプロパティを持つオブジェクトを作るとします。itemプロパティの型は任意です。

ジェネリックタイプを利用せず型ごとにクラスを定義する場合、実装は以下のようになります。

type StringItem = {
  item: string;
};

type NumberItem = {
  item: number;
};

const stringItem: StringItem = { item: "hello" };
const numberItem: NumberItem = { item: 10 };

console.log(stringItem.item);
// hello

console.log(numberItem.item);
// 10

ジェネリックタイプを利用すると以下のようになります。

// string型やnumber型で指定していた部分を型Tで共通化
type Item<T> = {
  item: T;
};

// <string>でitemプロパティの型をstring型に指定
const stringItem: Item<string> = { item: "hello" };

// <number>でitemプロパティの型をnumber型に指定
const numberItem: Item<number> = { item: 10 };

console.log(stringItem.item);
// hello

console.log(numberItem.item);
// 10

ジェネリックメソッド(ジェネリック関数)

ジェネリックメソッド(ジェネリック関数)とはジェネリクスの手法を取り入れたメソッド、つまり汎用的なメソッドを表現するものです。

例えば、受け取った値をそのまま返すメソッドを作るとします。受け取る値の型は任意です。

ジェネリックメソッドを利用せず型ごとにメソッドを定義する場合、実装は以下のようになります。

const returnNumber = (num: number): number => num;
const returnString = (str: string): string => str;

const str = returnString("hello");
const num = returnNumber(10);

console.log(str);
// hello

console.log(num);
// 10

ジェネリックメソッドを利用すると以下のようになります。
引数(())の直前にジェネリクス(<>)を配置することで、ジェネリクスによって引数の型が指定されるイメージです。

// string型やnumber型で指定していた部分を型Tで共通化
// 型Tのvalueを受け取り、型Tのvalueを返す
const returnValue = <T>(value: T): T => value;

// <string>で引数の型をstring型に指定
const str = returnValue<string>("hello");

// <number>で引数の型をnumber型に指定
const num = returnValue<number>(10);

console.log(str);
// hello

console.log(num);
// 10

ジェネリッククラス

ジェネリッククラスとはジェネリクスの手法を取り入れたクラス、つまり汎用的なクラスを表現するものです。

例えば、初期化に利用した値をgetItem()メソッドで取得できるインスタンスを作るとします。初期化で利用する値の型は任意です。

ジェネリッククラスを利用せず型ごとにクラスを定義する場合、実装は以下のようになります。

class StringItem {
  item: string;

  constructor(item: string) {
    this.item = item;
  }
  getItem(): string {
    return this.item;
  }
}

class NumberItem {
  item: number;

  constructor(item: number) {
    this.item = item;
  }
  getItem(): number {
    return this.item;
  }
}

const stringItem = new StringItem("hello");
const numberItem = new NumberItem(10);

console.log(stringItem.getItem());
// hello

console.log(numberItem.getItem());
// 10

ジェネリッククラスを利用すると以下のようになります。
引数(())の直前にジェネリクス(<>)を配置することで、ジェネリクスによって引数の型が指定されるイメージです。

// string型やnumber型で指定していた部分を型Tで共通化
class Item<T> {
  item: T;

  constructor(item: T) {
    this.item = item;
  }
  getItem(): T {
    return this.item;
  }
}

// <string>で引数の型をstring型に指定
const stringItem = new Item<string>("hello");

// <number>で引数の型をnumber型に指定
const numberItem = new Item<number>(10);

console.log(stringItem.getItem());
// hello

console.log(numberItem.getItem());
// 10

ジェネリクスの特徴

ジェネリクスの特徴について紹介します。

ジェネリクスでも型推論ができる

ジェネリクスでも型推論は有効であるため、ジェネリックメソッドやジェネリッククラスの引数の型の指定は省略できます。

型推論を活用したジェネリックメソッドの利用方法は以下の通りです。

const returnValue = <T>(value: T): T => value;

// returnValue<string>("hello"); の<string>は省略可能
const str = returnValue("hello");

型推論を活用したジェネリッククラスの利用方法は以下の通りです。

class Item<T> {
  item: T;

  constructor(item: T) {
    this.item = item;
  }
  getItem(): T {
    return this.item;
  }
}

// new Item<string>("hello"); の<string>は省略可能
const stringItem = new Item("hello");

ただし以下のように、ジェネリックタイプの型は省略できないので注意してください。

type ItemType<T> = {
  item: T;
};

// OK
const stringItemType: ItemType<string> = { item: "hello" };

// NG: コンパイルエラー
const stringItemType: ItemType = { item: "hello" };
//Generic type 'ItemType' requires 1 type argument(s).ts(2314)

複数の型を指定できる

<T, U>のように、カンマ区切りで型を並べることで複数の型を組み合わせたジェネリクスが定義できます。

以下は複数の型を利用したジェネリックタイプの例です。

// 型Tのkeyプロパティと、型Uのvalueプロパティを持つオブジェクト
type KeyValue<T, U> = {
  key: T;
  value: U;
};

const exampleKeyValue: KeyValue<string, number> = {
  key: "example",
  value: 10,
};

console.log(exampleKeyValue.key); // example
console.log(exampleKeyValue.value); // 10

ジェネリクスで引数に制約をつける方法

extendsを利用することでジェネリクスで引数に制約をつけられます。
以下ではextendsの利用方法について紹介します。

型の制約をつける場合

ジェネリックメソッドの引数をstring型もしくはnumber型のみ許可する場合は以下のように記述します。

const returnValue = <T extends number | string>(value: T): T => value;

// stringは number | string の条件を満たすので問題なし
const str = returnValue<string>("hello");

// numberは number | string の条件を満たすので問題なし
const num = returnValue<number>(10);

// booleanは number | string の条件を満たさないのでコンパイルエラーになる
const truthy = returnValue(true);
// Argument of type 'boolean' is not assignable to parameter of type 'string | number'.ts(2345)

console.log(str);
// hello

console.log(num);
// 10

console.log(truthy);
// コンパイルエラー

ジェネリックメソッドの引数をShape型のみ許可する場合は以下のように記述します。

type Square = {
  kind: "square";
  size: number;
};

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};

type Triangle = {
  kind: "triangle";
  width: number;
  height: number;
};

type Shape = Square | Rectangle;

const returnShape = <T extends Shape>(shape: T): T => shape;


const square: Square = { kind: "square", size: 5 };
const triangle: Triangle = { kind: "triangle", width: 5, height: 4 };

// Square型 は Shape型 に含まれているので問題なし
console.log(returnShape<Square>(square).size);

// Triangle型 は Shape型 に含まれているのでコンパイルエラー
console.log(returnShape<Triangle>(triangle).size);
// Type 'Triangle' does not satisfy the constraint 'Shape'.

プロパティの制約をつける場合

例えばsizeプロパティを参照するジェネリックメソッドを作るとします。その場合、ジェネリクスで定義する型Tにはsizeプロパティが必須になります。

プロパティの制約も型と同様にextendsで表現できます。

sizeプロパティを持つオブジェクトのみジェネリックメソッドの引数に許可する場合は以下のように記述します。

type Square = {
  kind: "square";
  size: number;
};

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};

const getShapeSize = <T extends { size: number }>(shape: T): number =>
  shape.size;

const square: Square = { kind: "square", size: 5 };
const rectangle: Rectangle = { kind: "rectangle", width: 5, height: 4 };

// Square には { size: number } が含まれているので問題ない
console.log(getShapeSize<Square>(square));
// 5

// Rectangle には { size: number } が含まれていないのでコンパイルエラーになる
console.log(getShapeSize<Rectangle>(rectangle));
//  Property 'size' is missing in type 'Rectangle' but required in type '{ size: number; }'.ts(2344)

参考: Array型の引数のみ許可したい場合

Array型のジェネリックメソッドを作成する場合は、引数の型をジェネリクスのArray(T[])で表現します。extendsの指定は不要です。
具体的には以下のようになります。

const arrayLength = <T>(array: T[]): number => {
  // lengthメソッドを実行するので、引数は型Tではなく配列型T[]を指定する必要がある
  return array.length;
};

const numberArray = [1, 2, 3];
console.log(arrayLength<number>(numberArray));
// 3

const stringArray = ["a", "b", "c", "d"];
console.log(arrayLength<string>(stringArray));
// 4

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考