TypeScriptジェネリクス入門: 型引数・extends制約・ユーティリティ型の使いどころ
TypeScriptのジェネリクスを、型引数の基本・extends制約・デフォルト型引数・Partialなどのユーティリティ型・実務の使い分けまで、コピペで動く例で整理。
「配列の重複を消す関数、作って」
そう頼んで返ってきたコードは、たしかに動きました。でも戻り値の型が any[] で、呼び出した先で .id を打っても補完が出ない。中身を確かめたら key: string で受けていて、存在しないキー名を渡してもエラーにならない。動くのに、型が何も守ってくれていなかったんです。
このとき足りなかったのが、ジェネリクス(generics)です。日本語だと「総称型」。要するに、型を後から外から差し込める仕組みのこと。これを使えていなかったから、関数は「どんなデータでも受け取る代わりに、何も覚えていない」状態になっていました。
僕も最初はこの記号 <T> が苦手でした。でも、つまずくのは記号ではなく「どの値を自由にして、どの値を縛るか」を決めていないからだと気づいてから、急に楽になりました。今日はそこを、身近な例で順番に解きほぐします。
この記事の要点
- ジェネリクスは「型を引数として受け取る」仕組み。
<T>は実行時の変数ではなく、コンパイル時だけ存在する型の箱。 extendsは継承ではなく制約。K extends keyof Tで「Tに実在するキーだけ許す」と型で宣言できる。- デフォルト型引数(
<T = string>)で、指定しなかったときの型をあらかじめ決められる。 Partial/Record/Pickなどのユーティリティ型は、中身がジェネリクスでできた「既製の型関数」。自作する前にまず探す。- 何でも
<T>にするのは逆効果。型引数が1か所でしか出てこないなら、たいてい不要。
ジェネリクスは「型の引数」だと思うと早い
関数の引数は知っていますよね。add(a, b) の a と b に、呼ぶたびに違う値を渡す。ジェネリクスはその型バージョンです。<T> という箱を用意しておいて、呼ぶたびに違う型を渡す。それだけです。
具体例で見ます。「渡したものをそのまま返す」関数を、まず型なしで書くとこうなります。
// Before: any で受けると、戻り値の型が消える
function identityAny(value: any): any {
return value;
}
const a = identityAny("hello"); // a の型は any(string ではない)
a.toFixed(2); // 文字列なのにエラーにならない。実行時に壊れる
any を使った瞬間、入力が文字列だったという事実が出口で消えます。だから .toFixed という数値用のメソッドを打ってもコンパイラが黙る。これがバグの温床です。
ジェネリクスで書き直すと、入口と出口が同じ型でつながります。
// After: T で受けると、入れた型がそのまま出てくる
function identity<T>(value: T): T {
return value;
}
const a = identity("hello"); // a の型は string に自動で決まる
const n = identity(42); // n の型は number に自動で決まる
a.toUpperCase(); // OK
// n.toUpperCase(); // number なのでエラーにしてくれる
<T> と書いておくと、呼び出し側の引数から型をコンパイラが推論します。identity("hello") なら T は string、identity(42) なら number。いちいち型を書かなくても、入れたものに合わせて変わる。ここがジェネリクスの気持ちよさです。
慣例で T(Type)、K(Key)、V(Value)、E(Element/Error)と一文字を使いますが、ただの名前なので <Item> でも構いません。チームで読みやすいほうを選べばいいです。
extends は「継承」じゃなくて「制約」
ジェネリクスの次の壁が extends です。クラスの継承で見る単語なので混乱しますが、ジェネリクスの文脈では意味が違います。ここでの extends は「この形を満たす型だけ受け取る」という縛りです。
冒頭の重複排除を例にします。素朴に書くと、キー名がただの文字列なので、存在しないキーを渡してもすり抜けます。
type User = { id: string; email: string; role: "admin" | "editor" };
// Before: key が string だと、存在しないキーも通ってしまう
function uniqueByLoose<T>(items: T[], key: string): T[] {
const seen = new Set();
return items.filter((item) => {
const v = (item as any)[key]; // any キャストが必要になる時点で怪しい
if (seen.has(v)) return false;
seen.add(v);
return true;
});
}
ここで K extends keyof T を足します。keyof T は「Tが持つプロパティ名の集合」(この場合 "id" | "email" | "role")。K extends keyof T は「Kはその集合のどれか」という制約です。
// After: keyof T で「実在するキーだけ」に縛る
function uniqueBy<T, K extends keyof T>(items: readonly T[], key: K): T[] {
const seen = new Set<unknown>();
const out: T[] = [];
for (const item of items) {
const value = item[key]; // キャスト不要。item[key] は安全に型がつく
if (seen.has(value)) continue;
seen.add(value);
out.push(item);
}
return out;
}
const users: User[] = [
{ id: "u_1", email: "[email protected]", role: "admin" },
{ id: "u_2", email: "[email protected]", role: "editor" },
{ id: "u_1", email: "[email protected]", role: "admin" },
];
const byId = uniqueBy(users, "id"); // OK
// uniqueBy(users, "name"); // "name" は User に無いのでコンパイルエラー
console.log(byId.map((u) => u.id));
item[key] に as any がいらなくなった点に注目してください。制約を付けたぶん、コンパイラが「key は必ず T のキーだ」と分かるので、安全にアクセスできる。制約は窮屈にするためではなく、安全なショートカットを手に入れるためにあります。
デフォルト型引数で「指定しなくても困らない」ようにする
関数の引数にデフォルト値(function f(x = 0))があるように、型引数にもデフォルトを置けます。<T = string> のように書くと、呼ぶ側が型を指定しなかったときに使われる型を決められます。
APIの結果を表す型でよく使います。エラーの型はたいてい共通なので、指定しなければ標準のエラー型を使う、という設計にしておくと呼び出しが軽くなります。
type ApiError = { code: string; message: string };
// E を省略したら ApiError を使う、というデフォルト
type ApiResult<T, E = ApiError> =
| { ok: true; data: T }
| { ok: false; error: E };
// E を書かなくていい。自動で ApiError になる
type UserResult = ApiResult<{ id: string; name: string }>;
function unwrap<T, E>(result: ApiResult<T, E>): T {
if (result.ok) return result.data;
throw new Error(JSON.stringify(result.error));
}
ApiResult<{ id: string }> と ApiResult<{ id: string }, ValidationError> の両方が書ける。よくあるケースは短く、特別なケースは細かく書き分けられるのがデフォルト型引数の利点です。
ユーティリティ型は「既製のジェネリクス」
ここまで来ると、TypeScriptに最初から入っている Partial や Record の正体が見えてきます。あれらは中身がジェネリクスで書かれた、出来合いの型関数です。自分で <T> をこねる前に、まずこの棚を覗くのが近道です。
| ユーティリティ型 | 何をするか | 使う場面 |
|---|---|---|
Partial<T> | 全プロパティを任意に | 更新用の差分オブジェクト |
Required<T> | 全プロパティを必須に | optionalを埋め切った後の型 |
Pick<T, K> | 指定キーだけ抜き出す | 一覧表示用の軽い型 |
Omit<T, K> | 指定キーを除く | パスワードを抜いた公開用型 |
Record<K, V> | キー型と値型から辞書を作る | 設定マップ、ID→値の対応表 |
ReturnType<F> | 関数の戻り値の型を取り出す | 既存関数に型を合わせる |
たとえば「ユーザー更新API」。全部のフィールドを必須にすると、名前だけ変えたいときに困ります。Partial を使えば一発です。
type User = { id: string; name: string; email: string };
// id 以外は「あってもなくてもいい」更新用の型を、Partial と Omit で組み立てる
type UserPatch = { id: string } & Partial<Omit<User, "id">>;
function patchUser(current: User, patch: UserPatch): User {
return { ...current, ...patch };
}
const u: User = { id: "u_1", name: "Masa", email: "[email protected]" };
const next = patchUser(u, { id: "u_1", name: "Masaki" }); // email は省略OK
console.log(next.name); // "Masaki"
Record も頻出です。「プラン名 → 月額」のような対応表に any を使う人が多いですが、Record<"free" | "pro", number> と書けば、キーの打ち間違いも値の型ミスも止められます。ユーティリティ型のもっと細かい使い分けはTypeScriptユーティリティ型入門: 実務の型を安全に作るにまとめたので、Partial と Pick で迷ったらそちらを見てください。
コピペで動く: 型まで守る設定ストア
ここまでの「型引数・extends制約・デフォルト型引数・ユーティリティ型」を1つにまとめた、小さな設定ストアを置きます。get で取った値の型が、キーごとに正しく決まるのがポイントです。そのまま npx tsc に通せます。
// store.ts — キーごとに値の型が変わる、型安全な設定ストア
type Schema = {
theme: "light" | "dark";
fontSize: number;
notify: boolean;
};
class Store<T extends Record<string, unknown>> {
// 初期値で全キーを埋めておく(Partial ではなく完全な T を要求)
constructor(private state: T) {}
// K でキーを縛るので、get の戻り値は T[K](キーごとに別の型)になる
get<K extends keyof T>(key: K): T[K] {
return this.state[key];
}
// 一部だけ更新したいので Partial<T> を受け取る
patch(diff: Partial<T>): void {
this.state = { ...this.state, ...diff };
}
}
const store = new Store<Schema>({
theme: "dark",
fontSize: 14,
notify: true,
});
const size = store.get("fontSize"); // number に確定
const theme = store.get("theme"); // "light" | "dark" に確定
store.patch({ fontSize: 16 }); // OK(一部だけでよい)
// store.patch({ fontSize: "16" }); // string はエラーで止まる
// store.get("color"); // 無いキーはエラーで止まる
console.log(size + 2, theme);
get("fontSize") の戻り値が number、get("theme") が "light" | "dark" と、呼ぶキーによって型が切り替わる。これがジェネリクスの本領です。検証したいときは下の手順で。tsc は構文チェックだけでなく、コメントアウトした行を戻すと「ちゃんとエラーになるか」も確かめられます。
npm install --save-dev typescript
npx tsc --noEmit --strict store.ts
日々の strict 設定や型テストの習慣はClaude CodeでTypeScript開発を速く安全にする実践Tipsに寄せました。ジェネリクスを書く前に、まず strict を入れておくのが土台になります。
いつ使わないか(ここが本題かもしれない)
ジェネリクスは便利なぶん、覚えたての頃ほど付けすぎます。僕もやりました。型引数が1か所にしか登場しないなら、それはたいてい不要です。
// 過剰: T は引数にしか出てこない。ただの unknown でいい
function logIt<T>(value: T): void {
console.log(value);
}
// これで十分
function logIt2(value: unknown): void {
console.log(value);
}
判断はシンプルで、入口と出口、または複数の引数で「同じ型」を結びたいときだけ使う。重複排除(引数の配列と戻り値の配列が同じ T)や設定ストア(キーと戻り値が T[K] で連動)は、まさにそれです。一方、ただ受け取って中で消費するだけなら unknown を絞るほうが読みやすい。「賢く見せる型」より「次に読む人が分かる型」を選んでください。
よくある質問
Q. <T> の T という名前は決まり?
いいえ。ただの識別子なので <Item> でも <TData> でも動きます。慣例で T/K/V を使うだけです。意味が伝わるなら長い名前のほうが親切なこともあります。
Q. extends は継承のことですか?
ジェネリクスの中では違います。K extends keyof T は「Kはこの型の範囲に収まる」という制約です。クラスの extends(継承)とは別物だと割り切ると混乱しません。
Q. Partial などは自分で作らないとダメ?
いりません。Partial Pick Omit Record などは標準で入っています。中身はジェネリクスで書かれた既製品なので、自作する前に公式のユーティリティ型を探すのが先です。
Q. any と unknown と <T> はどう使い分ける?
any は型チェックを捨てるので原則避けます。中身が本当に不明なら unknown で受けて使う前に絞る。入口と出口の型を連動させたいなら <T> です。
Q. ジェネリクスはコンパイル後のJavaScriptに残りますか?
残りません。型情報はコンパイル時に消えます(型消去)。実行時の分岐に使いたい場合は、別途 typeof などのランタイム判定が必要です。
まとめ
ジェネリクスは、難しい記号の遊びではなく「型を引数として受け渡し、入口と出口をつなぐ」道具です。順番はいつも同じで、まず <T> で型を受け取り、extends で許す範囲を縛り、デフォルト型引数でよくあるケースを短くし、既製の Partial/Record で済むならそれを使う。そして、型引数が1か所しか出てこないなら潔く外す。
冒頭の「型が any[] の重複排除」は、<T, K extends keyof T> を足しただけで、戻り値に補完が戻り、存在しないキーがコンパイルで止まるようになりました。賢い型を盛るより、入口と出口がちゃんとつながっているか。そこだけ意識すると、ジェネリクスは急に味方になります。まずは上の設定ストアを tsc に通して、コメントアウトした行を1つずつ戻し、どこで赤線が出るかを目で確かめてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。