はじめに
TypeScriptを書いていると、Partial<User>、Array<string>、Record<string, number> のような < > が付いた記法を見かけることがあります。
type UpdateUser = Partial<User>;
const names: Array<string> = ['Taro', 'Hanako'];
const scores: Record<string, number> = {
math: 80,
english: 90,
};
これらの < > を使った記法は、Generics(ジェネリクス) と呼ばれます。
Genericsを理解すると、TypeScriptが提供するユーティリティ型を正しく使えるようになるだけでなく、自分で型安全かつ再利用性の高い関数や型を作れるようになります。
この記事では、Genericsの基本的な考え方や、Genericsを使うメリットについてまとめます。
Genericsとは?
Genericsとは、型を後から決められるようにする仕組みです。
例えば、文字列をそのまま返す関数を考えます。
function getValue(value: string): string {
return value;
}
const result = getValue('Hello');
この関数は文字列しか扱えません。
数値も扱いたい場合、次のようにany型を指定したくなります。
function getValue(value: any): any {
return value;
}
const result = getValue(100);
↑の場合、resultはany型になるため、どのようなプロパティやメソッドにもアクセスできてしまいます。
しかし、any型を使うと型安全性が失われてしまいます。
そこでGenericsを使用します。
function getValue<T>(value: T): T {
return value;
}
T は「あとで決める型」を表しています。
この関数を呼び出すと、
const str = getValue('Hello');
const num = getValue(100);
のように、呼び出し時の型に応じて T の中身が決まります。
このとき、TypeScriptは引数から型を推論し、strとnumをそれぞれ以下の型として扱います。
str → string
num → number
と推論されます。
また、明示的に型を指定することもできます。
getValue<string>('Hello');
getValue<number>(100);
Genericsを使うメリット
型安全性を維持できる
anyの場合
function getValue(value: any): any {
return value;
}
const result = getValue('abc');
result.toFixed();
toFixed()は number型でしか使用できないメソッドですが、resultがany型になっているため、TypeScriptは誤りを検知できません。
その結果、コンパイルが通ってしまい実行時エラーの原因になります。
Genericsの場合
function getValue<T>(value: T): T {
return value;
}
const result = getValue('abc');
result.toFixed();
resultがstring型として推論されるため、誤ったメソッド呼び出しをコンパイル時に検知できます。
これにより、実行時エラーを未然に防げます。
同じ処理をさまざまな型で再利用できる
配列の先頭要素を取得する関数を作ります。
function getFirst<T>(items: T[]): T {
return items[0];
}
使用例
const name = getFirst(['A', 'B']);
const id = getFirst([1, 2, 3]);
このとき、nameとidはそれぞれ、
name → string
id → number
となります。
Genericsを使うことで1つの関数で複数の型に対応できます。
複数の型パラメータを使う
Genericsは複数指定することもできます。
function createPair<T, U>(first: T, second: U) {
return {
first,
second,
};
}
const pair = createPair('id', 100);
型は以下になります。
{
first: string;
second: number;
}
T や U は慣例的に使われる名前ですが、意味のある名前を付けることもできます。
function createPair<Key, Value>(
key: Key,
value: Value,
) {
return {
key,
value,
};
}
extendsで型に制約を付ける
Genericsは、基本的にどのような型でも受け取れます。
例えば、渡された値の文字数や要素数を取得したいとします。
function getLength<T>(value: T): number {
return value.length;
}
しかし、T にはlengthが存在しないnumberやbooleanが渡される可能性もあります。
TypeScriptは「Tにlengthがあるとは限らない」と判断し、value.lengthを使用できません。
そこで、length を持つ型だけを受け取れるように制約を付けます。
function getLength<T extends { length: number }>(
value: T,
): number {
return value.length;
}
T extends { length: number } は、
lengthを持つ型だけを
Tとして受け付ける
という意味です。
そのため、numberやbooleanの値が引数に入るとコンパイルエラーになります。
まとめ
Genericsは、型を後から決定できる仕組みです。
Genericsについて理解することで、ライブラリが提供する型の意味を理解しやすくなり、型安全で再利用性の高いコードを書けるようになります。
参考