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においてかなり重要な機能なので、早めにマスターしたいですね!