2
2

TypeScriptのジェネリクスってなんだ?

Posted at

はじめに

4月からTypeScriptを初めて使いだしてから一ヶ月ほど経ちますが、いまだに以下のようなコードに馴染めないでいます。

function reverseArray<T>(arr: T[]): T[] {
  return arr.reverse();
}

const numbers = [1, 2, 3];
const reversedNumbers = reverseArray<number>(numbers);
console.log(reversedNumbers);

これは「ジェネリクス」と呼ばれる書き方を使っています。

本記事はジェネリクスについての基礎から丁寧に確認する記事となります。

ジェネリクスとは

ジェネリクスの概要

ジェネリクスは再利用できるコンポーネントを作るための機能です。
特定の型ではなく、様々な型で動作するコンポーネントを作成します。
TypeScript独自の機能ではなく、C#やJavaなど他の言語にもジェネリクスは存在します。
ジェネリクスは関数のほか、クラス、インターフェース、型エイリアスでも使用することができます。

ジェネリクスの定義方法

ジェネリクスを使用するには定義時に<>を使用します。
冒頭で示したコードを使って確認します。

function reverseArray<T>(arr: T[]): T[] {
  return arr.reverse();
}

const numbers = [1, 2, 3];
const reversedNumbers = reverseArray<number>(numbers);
console.log(reversedNumbers); // [ 3, 2, 1 ]

関数でジェネリクスを使用する場合は、引数のかっこの前に<>を使います。
<>内に指定する文字列は自由ですが、Tを指定するのが慣例のようです。また、他にはKVEが使われるようです。
(おそらく型のType、キーのKey、値のValue、要素のElement)

指定した文字列を関数内で型として扱えます。
例示ではT型の配列を引数として受け取り、T型の配列を戻り値として返すようにしています。

関数を使用する場合には、定義するときと同じように引数のかっこの前に<>を使います。
関数の中で使用したい型を指定します。
例示ではnumber型を扱いたいので、numberを指定しています。
なお、型推論ができる場合には関数呼び出し時の<>を省略することができます。

ジェネリクスの意味

ジェネリクスを使わない場合、どのようになるかを考えます。

ジェネリクスを使わない例
function reverseNumArray(arr: number[]) {
  return arr.reverse();
}
function reverseStrArray(arr: string[]) {
  return arr.reverse();
}
const numbers = [1, 2, 3];
const strings = ["a", "b", "c"];

const reversedNumbers = reverseNumArray(numbers);
const reversedStrings = reverseStrArray(strings);

console.log(reversedNumbers); // [ 3, 2, 1 ]
console.log(reversedStrings); // [ 'c', 'b', 'a' ]

本来はわざわざ関数を作るほどの処理ではないのですが、理解を深めるためにこのような処理とします。
引数には型を指定する必要があるため、数値用の処理、文字列用の処理とそれぞれ別に定義しています。
やっていることは変わらないのに、型の分だけ処理を用意するのは冗長です。

上記を避けるため、以下のように処理を定義することもできます。

any型を使用して型定義を柔軟にする例
function reverseAnyArray(arr: any[]) {
  return arr.reverse();
}
const numbers = [1, 2, 3];
const strings = ["a", "b", "c"];

const reversedNumbers = reverseAnyArray(numbers);
const reversedStrings = reverseAnyArray(strings);

console.log(reversedNumbers); // [ 3, 2, 1 ]
console.log(reversedStrings); // [ 'c', 'b', 'a' ]

引数の型をany型としています。
一見これは良さそうに思えますが、戻り値として返されるのはany型の配列です。
any型は型チェックが行われないため、思わぬバグの原因となったり、IDEでの型補完の恩恵を受けられなくなったりします。

そこで登場するのがジェネリクスです。

ジェネリクスを使うことで、型を引数のようにして関数に受け渡す事ができます。
関数の定義時にはT型として型の定義は柔軟なものにしています。
そして実行時にどの型で実行するのかを指定します。

ジェネリクスの使用例
function reverseArray<T>(arr: T[]): T[] {
  return arr.reverse();
}

const numbers = [1, 2, 3];
const strings = ["a", "b", "c"];

const reversedNumbers = reverseArray<number>(numbers);
const reversedStrings = reverseArray(strings);

console.log(reversedNumbers); // [ 3, 2, 1 ]
console.log(reversedStrings); // [ 'c', 'b', 'a' ]

様々なジェネリクス

アロー関数

アロー関数でジェネリクスを使用する場合、ほとんど関数でジェネリクスを使用するのと変わりません。

アロー関数でのジェネリクス
const reverseArray = <T>(arr: T[]): T[] => {
  return arr.reverse();
};

引数のかっこの前に<>で型を定義するだけです。

クラス

クラスでジェネリクスを使用する場合は、クラス名のあとに<>を使います。
以下は任意の型を受け取るスタックを定義した例です。

クラスでのジェネリクス
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2

const stringStack = new Stack<string>();
stringStack.push("Hello");
stringStack.push("World");
console.log(stringStack.pop()); // "World"

T型の配列をプライベート変数として用意し、T型の値を受け取るpushメソッド、T型の値を返すpopメソッドを定義しています。
これにより、数値型や文字列型などそれぞれの型にあわせたスタックを作成することができます。

インターフェース

インターフェースでジェネリクスを使用する場合、インターフェース名のあとに<>を使います。
以下はあるオブジェクトを作成できるインターフェースを定義した例です。

インターフェースでのジェネリクス
interface Pair<K, V> {
  key: K;
  value: V;
}

const pair1: Pair<string, number> = {
  key: "count",
  value: 42,
};

const pair2: Pair<number, boolean> = {
  key: 123,
  value: true,
};

keyというプロパティとvalueというプロパティを定義しています。
それぞれK型とV型であることを指定しています。
ジェネリクスでは、カンマ区切りで複数の型パラメータを指定することができます。
文字列と数値、数値と真偽値など任意の組み合わせでkeyvalueの型を定義することができます。

型エイリアス

型エイリアスで使用する場合、エイリアス名のあとに<>を使います。
以下は任意の型に対してnullを許容する型エイリアスを定義した例です。

type Nullable<T> = T | null;

const value1: Nullable<string> = "Hello";
const value2: Nullable<number> = null;

T型またはnull型であるNullableを定義することで、任意の型に対してnullを入れてもエラーにならない型を作成することができます。

ジェネリクスへの制約

ジェネリクスを使用することで型の定義を柔軟にすることができますが、自由すぎると逆に困る場合もあります。
そこで、ジェネリクスには制約を設けることができます。
以下は、extendsキーワードを使い、ジェネリクスに対してあるプロパティを持つことを強制する例です。

ジェネリクスへの制約
interface Lengthwise {
  length: number;
}

function getLength<T extends Lengthwise>(obj: T): number {
  return obj.length;
}

const str = "Hello";
console.log(getLength(str)); // 5

const arr = [1, 2, 3];
console.log(getLength(arr)); // 3

const num = 42;
console.log(getLength(num)); // エラー

引数で指定した値のlengthを定義する関数を作成しました。
引数の型はT型としていますが、<>内でLengthwiseインターフェースをextendsしています。
これにより、T型は任意の型ではありますが、number型のlengthプロパティをもっている必要があります。

文字列や配列はlengthプロパティをもっていますが、数値はもっていません。
そのため、数値を引数に指定した場合はエラーとなります。

このように制約を設けることで、ある程度型に柔軟性をもたせつつ、型安全な処理を作成することができます。

まとめ

ジェネリクスの概要から、いろいろなところで使えるジェネリクスについて確認しました。
何となく何をしているかわからず、苦手意識がありましたが、要は引数のように型を受け渡すことで、処理を柔軟にしているものだ、ということが理解できました。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2