こんにちは、かつコーチです。
前回はジェネリクスの基本を扱い、型を引数のように渡す考え方を紹介しました。
今回はその一歩先、ジェネリクスの制約(extends)を扱います。
「ジェネリクスは書けるようになったけど、T に対して自由にプロパティへアクセスできなくて困った」という人は多いはずです。
私も最初にジェネリクスを実務で使ったとき、まさにこの壁にぶつかりました。
ジェネリクス制約とは?
制約なしジェネリクスの限界
ジェネリクスの型パラメータ T は、何も制約をつけなければ「どんな型でも受け取れる」状態になります。
裏を返せば、TypeScriptは T の中身について一切の情報を持っていないということです。
function getLength<T>(item: T): number {
return item.length; // エラー:Property 'length' does not exist on type 'T'.
}
item が文字列や配列であれば .length は存在しますが、T は数値やオブジェクトかもしれません。
TypeScriptは「T に length があるとは限らない」と判断するため、コンパイルエラーになります。
extendsで制約をかける基本構文
ここで使うのが extends です。
T extends 何らかの型 と書くことで、「T は少なくともこの型の形を持っている」とTypeScriptに約束できます。
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(item: T): number {
return item.length; // OK:T は必ず length を持つと保証されている
}
console.log(getLength("かつコーチ")); // 5
console.log(getLength([1, 2, 3])); // 3
HasLength インターフェースに length: number を定義し、T extends HasLength とすることで、item.length へ安全にアクセスできるようになりました。
文字列や配列のように length プロパティを持つ型なら、何でも渡せる汎用的な関数が完成します。
実践的な制約の書き方
プロパティの存在を保証する制約
実務でよく使うのは、「特定のプロパティを持つオブジェクト」に制約する書き方です。
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
type User = { id: number; name: string };
const users: User[] = [
{ id: 1, name: "かつコーチ" },
{ id: 2, name: "ゲスト" },
];
const found = findById(users, 1);
console.log(found?.name); // かつコーチ
T extends HasId としておくことで、User に限らず id: number を持つあらゆる型の配列に対して、同じ findById 関数を使い回せます。
戻り値も T のまま返るので、呼び出し側では User 型として扱われ、name プロパティにも問題なくアクセスできます。
keyofと組み合わせて安全にプロパティアクセスする
制約は keyof と組み合わせると、さらに強力になります。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "かつコーチ", age: 34 };
const name = getProperty(user, "name"); // 型は string
const age = getProperty(user, "age"); // 型は number
K extends keyof T と書くことで、「key は T に実在するプロパティ名でなければならない」という制約になります。
存在しないプロパティ名を渡すと、その場でコンパイルエラーとして検出できます。
getProperty(user, "email"); // エラー:Argument of type '"email"' is not assignable to parameter of type '"id" | "name" | "age"'.
タイプミスや存在しないキーを実行前に発見できるのは、ジェネリクス制約の大きなメリットです。
つまずきやすいポイント・エラー対処
制約なしでプロパティアクセスしようとして詰まった話
私が実際につまずいたのは、汎用的な「配列をIDでグルーピングする関数」を作ろうとしたときでした。
❌ Before:制約なしで書いてエラーになった最初のコード
function groupById<T>(items: T[]): Record<number, T> {
const result: Record<number, T> = {};
for (const item of items) {
result[item.id] = item; // エラー:Property 'id' does not exist on type 'T'.
// TS2339: Property 'id' does not exist on type 'T'.
}
return result;
}
「どんな配列でもグルーピングできる関数にしたい」という気持ちが先行し、T に何の制約もつけずに item.id を使ってしまったのが原因です。
エラーメッセージだけを見ても最初は「なぜ id が使えないのか」がピンと来ず、しばらく型定義を眺めて考え込みました。
✅ After:extendsで「idを持つ型」に制約してから使う
interface HasId {
id: number;
}
function groupById<T extends HasId>(items: T[]): Record<number, T> {
const result: Record<number, T> = {};
for (const item of items) {
result[item.id] = item; // OK:T は必ず id を持つと保証されている
}
return result;
}
const products = [
{ id: 101, name: "商品A" },
{ id: 102, name: "商品B" },
];
const grouped = groupById(products);
console.log(grouped[101].name); // 商品A
「汎用的にしたい=制約をつけない」ではなく、「必要最低限の形だけを制約として明示する」のが正解でした。
HasId のような最小限のインターフェースを制約に使うことで、汎用性を保ちながら型安全性も両立できます。
応用・一歩先の使い方
複数の型パラメータに制約をかける
型パラメータは複数持つこともでき、それぞれに個別の制約をかけられます。
interface Named {
name: string;
}
interface Aged {
age: number;
}
function introduce<T extends Named, U extends Aged>(person: T, pet: U): string {
return `${person.name}さんが飼っているペットは${pet.age}歳です`;
}
introduce({ name: "かつコーチ" }, { age: 3 });
T と U それぞれに別の制約をかけることで、複数の引数間で異なる形を保証しながら組み合わせられます。
デフォルト型引数と組み合わせる
制約にはデフォルト値も設定できます。
interface ApiResponse<T extends object = Record<string, unknown>> {
status: number;
data: T;
}
const res1: ApiResponse = { status: 200, data: { message: "ok" } };
const res2: ApiResponse<{ userId: number }> = { status: 200, data: { userId: 1 } };
T extends object = Record<string, unknown> とすることで、型引数を省略した場合のフォールバック先を用意しつつ、object という最低限の制約も同時に維持できます。
API レスポンスの共通型を定義するときなど、汎用性と安全性を両立したい場面でよく使うパターンです。
まとめ
この記事のポイント
- 制約なしのジェネリクス
Tには、プロパティへ自由にアクセスできない T extends インターフェースで、「最低限この形は持っている」という約束をつけられるK extends keyof Tを使うと、存在しないプロパティ名をコンパイル時に検出できる- 汎用性を保ちたいときほど、必要最小限のインターフェースを制約として使うのがコツ
- 型パラメータは複数持てて、それぞれに個別の制約やデフォルト値を設定できる
次に読むべき記事
制約を使いこなせるようになったら、次は既存の型を加工する「ユーティリティ型」を見ていきましょう。
→ 次の記事:ユーティリティ型入門:Partial・Pick・Omitの使い方