TypeScriptでジェネリクスを使った汎用的な関数ユーティリティの作り方
生徒
「先生、TypeScriptで同じような処理をいろんな型で使いたいときって、毎回別々に関数を書かないといけないんですか?」
先生
「実は、そんなときに便利なのが“ジェネリクス(Generics)”という仕組みなんですよ。」
生徒
「ジェネリクス…? なんだか難しそうな名前ですね。どんなことができるんですか?」
先生
「ジェネリクスを使うと、型に依存しない“汎用的な関数”を作ることができます。つまり、どんな型でも使える便利な関数を作れるようになるんです。」
生徒
「なるほど!それなら同じ処理を何度も書かなくて済みそうですね!」
先生
「そうです。それでは、TypeScriptでジェネリクスを使った関数ユーティリティの作り方を、順番に見ていきましょう!」
1. ジェネリクスを使うと何が便利なの?
TypeScriptのジェネリクス(Generics)とは、「型をあとから決められる仕組み」です。通常の関数では、引数の型を決め打ちで書きますが、ジェネリクスを使うと「どんな型でも受け取れる」関数を作ることができます。つまり、型の再利用性が高まり、エラーを減らしながら柔軟なコードが書けるようになります。
たとえば、配列の最初の要素を取得する関数を考えてみましょう。文字列の配列にも、数値の配列にも対応したいときにジェネリクスが活躍します。
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
const numbers = [10, 20, 30];
const words = ["apple", "banana", "cherry"];
console.log(getFirstElement(numbers)); // 10
console.log(getFirstElement(words)); // "apple"
この例では、<T>という部分がジェネリクスの型パラメータです。Tには、呼び出し時に渡した配列の型が自動的に推論されます。つまり、numbersを渡したときはTがnumberになり、wordsを渡したときはstringになります。
2. 型推論でジェネリクスが自動的に決まる仕組み
TypeScriptはとても賢く、引数から自動的に型を推論します。上記のgetFirstElement関数では、わざわざ型を明示しなくてもTypeScriptが勝手に型を判断してくれます。
つまり、次のように書いても同じ結果になります。
console.log(getFirstElement<number>([1, 2, 3]));
console.log(getFirstElement([1, 2, 3])); // 型推論でもOK
ジェネリクスを使うと、「型安全(Type Safety)」を保ちながら、柔軟にいろんなデータ型に対応できるのが大きなメリットです。
3. 複数の引数に同じ型を適用する関数を作る
例えば「2つの値を配列にして返す関数」を作りたい場合、どんな型でも受け取れるようにしたいですよね。ジェネリクスを使うと、それが簡単に実現できます。
function makePair<T>(a: T, b: T): T[] {
return [a, b];
}
console.log(makePair(1, 2)); // [1, 2]
console.log(makePair("A", "B")); // ["A", "B"]
このように書くことで、引数の型を1つに統一しながら、さまざまな型のデータを受け取ることができます。もし型が異なるものを渡すと、TypeScriptがエラーを出してくれるので、間違いも防げます。
4. 型が異なるものを扱いたいときは複数のジェネリクスを使う
次に、「異なる型の2つの値を扱いたい」場合を考えてみましょう。そのときは、<T, U>のように、複数のジェネリクスを使うことができます。
function makeTuple<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
console.log(makeTuple("Hello", 123)); // ["Hello", 123]
このように、ジェネリクスを複数指定することで、より柔軟な汎用関数を作ることができます。ユーティリティ関数(Utility Function)として、実務でもよく使われる形です。
5. 現実的な例:配列の要素をシャッフルするユーティリティ関数
では、もう少し実用的な例を見てみましょう。配列の中身をランダムに並び替える「シャッフル関数」を作ってみます。どんな型の配列でも使えるように、ジェネリクスを使います。
function shuffleArray<T>(array: T[]): T[] {
return array
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map((obj) => obj.value);
}
console.log(shuffleArray([1, 2, 3, 4, 5]));
console.log(shuffleArray(["A", "B", "C", "D"]));
このようにジェネリクスを使うと、数値の配列でも文字列の配列でも、同じ関数でシャッフルが可能です。これがまさに“汎用的な関数ユーティリティ”の良い例です。
6. さらに応用:ジェネリクスで安全なデータ取得関数を作る
例えば、オブジェクトから特定のプロパティを取得する関数を考えてみましょう。普通に書くとミスをしやすいですが、ジェネリクスとキー制約(keyof)を組み合わせることで安全な関数が作れます。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "太郎", age: 25 };
console.log(getProperty(user, "name")); // "太郎"
この関数は、存在しないプロパティ名を指定するとエラーになります。つまり、型の安全性を保ったまま、柔軟にデータを扱えるのです。
7. 汎用ユーティリティを使うメリット
ジェネリクスを使って汎用的な関数ユーティリティを作ることで、次のようなメリットがあります。
- 同じような処理を何度も書かなくて済む
- 型のミスを防げる(型安全)
- コードの見通しがよくなる
- 保守や修正が楽になる
プログラミングに慣れてくると、共通処理を関数にまとめて再利用することが増えます。そんなとき、TypeScriptのジェネリクスはとても強力な味方になります。
まとめ
ジェネリクスを使った汎用的な関数ユーティリティの理解を深めよう
今回の記事では、TypeScriptにおけるジェネリクスの基本から始まり、汎用的な関数ユーティリティを作成する具体的な方法までを順を追って学びました。ジェネリクスは「型をあとから決められる仕組み」であり、同じ処理をさまざまな型に対して安全に再利用できる点が最大の特徴です。これにより、コードの重複を減らし、保守性と可読性を高めることができます。
特に、配列の要素を取得する関数や、複数の値をまとめる関数、異なる型を扱うタプル生成関数などは、実務でも頻繁に登場する処理です。ジェネリクスを使わずに実装すると、型ごとに似たような関数を何度も書くことになりがちですが、ジェネリクスを活用することで一つの関数で柔軟に対応できるようになります。これはTypeScriptならではの大きな利点と言えるでしょう。
型推論と型安全がもたらす安心感
ジェネリクスのもう一つの重要なポイントは、TypeScriptの強力な型推論と組み合わせて使える点です。関数呼び出し時に明示的に型を指定しなくても、引数の内容から自動的に型が決定されるため、コードはシンプルで読みやすくなります。それでいて、誤った型を渡した場合にはコンパイル時にエラーとして検出されるため、実行前にミスを防ぐことができます。
また、複数のジェネリクスや keyof を使ったキー制約を組み合わせることで、オブジェクト操作も非常に安全になります。存在しないプロパティにアクセスしようとした場合でも、TypeScriptが即座に警告を出してくれるため、バグの混入を未然に防ぐことができます。これらの仕組みは、大規模なコードベースやチーム開発において特に効果を発揮します。
汎用ユーティリティ関数のサンプルまとめ
function wrapValue<T>(value: T): T[] {
return [value];
}
function mergeArrays<T>(a: T[], b: T[]): T[] {
return [...a, ...b];
}
const wrappedNumber = wrapValue(10);
const wrappedString = wrapValue("サンプル");
const mergedNumbers = mergeArrays([1, 2], [3, 4]);
const mergedStrings = mergeArrays(["A"], ["B", "C"]);
このようなユーティリティ関数は、一度作っておけばさまざまな場面で再利用できます。ジェネリクスを使っているため、数値でも文字列でもオブジェクトでも同じ関数を安全に利用できます。小さな関数でも積み重ねることで、プロジェクト全体の品質を大きく向上させることができます。
生徒
「ジェネリクスを使うと、同じ関数をいろんな型で使える理由がやっと分かりました。型ごとに関数を作らなくていいのは便利ですね。」
先生
「その通りです。ジェネリクスは“型の再利用”を可能にする仕組みなので、コードをシンプルに保つのにとても役立ちます。」
生徒
「型推論のおかげで、書き方も思ったより難しくなかったです。エラーも早い段階で分かるのが安心ですね。」
先生
「TypeScriptは、正しく型を書くことで開発者を助けてくれます。ジェネリクスはその中でも特に重要な機能の一つです。」
生徒
「これからは、共通処理を見つけたらジェネリクスでユーティリティ関数にできないか考えてみます。」
先生
「それはとても良い考え方です。ジェネリクスを意識して使えるようになると、TypeScriptで書くコードの質が一段上がりますよ。」