JavaScript

ジェネリクス(Generics)の基本をマスターしよう

TypeScriptの機能の1つであるジェネリクス(Generics)について、具体的なコードを用いてまとめました。
ジェネリクスの基礎をしっかり抑えて、TypeScriptをマスターしていきましょう!

ジェネリクス(Generics)とは

ジェネリクス(Generics)はTypeScriptの機能の1つで、関数や型エイリアス(Type Aliases)に、外部から型情報を定義できるようする機能です。

ジェネリクスが解決する問題

例えば、下記のようにstring型の引数2つとって、その引数のどちらかを返す関数があったとします。

function getRandomString(a: string, b: string): string {
  return Math.random() <= 0.5 ? a : b;
}

上記のgetRandomString()で、number型を受け取るように変更したい場合、関数をコピーして型をnumberに変更するのは、ロジックが重複するので良くありません。

function getRandomString(a: string, b: string): string {
  return Math.random() <= 0.5 ? a : b;
}

// 型情報のみが異なる関数を用意する? → ロジックが重複するのでNG
function getRandomNumber(a: number, b: number): number {
  return Math.random() <= 0.5 ? a : b;
}

ジェネリクスを使用すると、型情報を外部から変数として渡すことができるため、下記のように記述できます。

function chooseRandomly<T>(a: T, b: T): T {
  return Math.random() <= 0.5 ? a : b;
}

// 使用する時は関数名の後ろに<>を使って定義する
chooseRandomly<string>("yes", "no");
chooseRandomly<number>(1, 2);

ジェネリクスの使い方

関数や型エイリアスに型引数を定義するには、関数名や型エイリアスの名称の後ろに括弧(<>)と型変数を記述します。
また、<T, K, U>のように、複数の型変数をカンマ区切りで指定することも可能です。

型変数の名称については、Typeを意味する<T>が慣習としてよく使われますが、名称はなんでも大丈夫です。
慣習としてT(Type)の他に、E(Element)、K(Key)、U(Unknown)がよく使われます。

// 関数の場合
function chooseRandomly<T>(a: T, b: T): T { /* */ }

chooseRandomly<string>("yes", "no");
chooseRandomly<number>(1, 2);


// 複数の型変数を定義
function chooseRandomly<T, K>(a: T, b: K) { /* */ }

chooseRandomly<string, number>('hoge', 1)


// 型エイリアスの場合
type Foo<T> = {
  value: T
}

const foo1: Foo = {
  value: 'text',
}
const foo2: Foo<number> = {
  value: 111,
}

型引数に初期値を設定する

型引数に初期値を設定するには、型変数の後ろに初期値を記述します。

// 関数の場合
function chooseRandomly<T = string>(a: T, b: T): T { /* */ }

chooseRandomly("yes", "no");

// 型エイリアスの場合
type Foo<T = string> = {
  value: T
}

const foo1: Foo = {
  value: 'text',
}

型推論(暗黙的な型解決)

関数の場合、型引数を指定しなくても、TypeScriptが推論してくれます。

function foo<T>(arg: T) {
  return { value: arg };
} 

const foo1 = foo('text');
/** 推論結果
  const foo1: {
    value: string;
  }
*/

const foo2 = foo(1);
/** 推論結果
  const foo2: {
    value: number;
  }
*/

型がnullになる可能性がある場合には、ユニオン型(Union Type)を使用するなどして、下記のように記述できます。

function foo<T>(arg: T) {
  return { value: arg };
} 

// nullが紛れ込むかもしれないのでユニオン型で指定
const foo4 = foo<string | null>(variable);

型に制約を追加する

extendsを用いて型引数の型を制限することができます。

/* 関数の場合 */
// 引数の型をstringのみに制限
function foo<T extends string>(arg: T) {
  return { value: arg };
} 

// ユニオン型も指定可能
function foo<T extends string | number | string[]>(arg: T) {
  return { value: arg };
} 


/* 型エイリアスの場合 */
type Foo<T extends string | number> = {
  value: T
}

const foo1: Foo<string> = {
  value: 'text',
}

const foo2: Foo<number> = {
  value: 111,
}

型変数の型制約と初期値を同時に設定する

型変数の型制約と初期値を同時に設定することもできます。
初見だと少し複雑に見えますが、ひとつひとつの要素を理解して見ていけば大丈夫です。

// extendsに初期値を設定する場合、最後に=で指定する

// 型エイリアス
type Foo<T extends string | number = string> = {
  value: T
}

// 関数の場合
function foo<T extends string | number = string>(arg: T) {
  //
}

型制約時の関数のサジェスト

関数の場合、エディターのサジェストが非常に便利です。
例えば、下記のようにユニオン型の型制約がある関数の場合、if文で型を絞り込むことで、絞り込まれた型のメソッドやプロパティがサジェストされます。

function foo<T extends string | number>(arg: T) {
	// 引数の型を絞り込むことができる
  if (typeof arg === 'string') {
		// ここのブロックではargはstring型
    return { value: arg.toUpperCase() }; // string型のサジェストがでてくる!
  }
  return { value: arg };
}

まとめ

ジェネリクスの型制約と初期値の同時指定などは、初見では複雑に見えてとっつきにくいですが、1つ1つの要素を順番に理解していくことが重要ですね。
TypeScriptにおいてかなり重要な機能なので、早めにマスターしたいですね!