【TypeScript】ジェネリクス(generics)完全理解|型引数Tと制約extendsの罠
TypeScript genericsは、型を引数のように受け取って重複なく安全にコードを使い回すための仕組みですが、<T>という見た目に身構えてしまう方は少なくありません。
<T>って結局なんなの?毎回コピペで雰囲気で書いている…
anyにしてしまうけど、本当はもっと安全に書きたい。
extendsで制約を付けるとエラーが出る理由がわからず、なんとなく回避している。
anyで逃げてきたコードを、型の安全性を保ったまま使い回せる形に書き換えられるようになります。基本構文から複数の型引数、制約の付け方、型推論が効く条件、そしてanyとの違いまで、実際のtscの出力やエラーと一緒に整理していきます。
この記事は次のような方におすすめです。
- 基本型は書けるが
<T>を見ると身構えてしまう中級入口の方 - 型が面倒で
anyに逃げがちで、安全な使い回しを身につけたい方 extendsによる制約や型推論を実務で使いこなしたい方- ジェネリクスとany・unknownの違いを腹落ちさせたい方
読み終える頃には、ジェネリクスを自分で書き起こし、どこで制約を付け、どこで型引数を省略できるかを判断できるようになります。
それでは、順を追って詳しく見ていきましょう!
- 未経験で後悔したくない
【実体験】未経験からITエンジニアに転職して後悔した話|4社経験してわかった「最初の選択ミス」 - 年収が低くて不安
4年間ずっと年収260万だったエンジニアが、転職で510万になるまでの全記録
ジェネリクスとは何か|型を引数として渡す仕組み
ジェネリクス(generics)とは、型そのものを引数のように受け取り、使うときに具体的な型を当てはめる仕組みです。日本語では「総称型」とも呼ばれ、ひとつの定義を複数の型で安全に使い回すために用います。
値を引数で受け取る関数と同じ発想で、型を後から差し込めるようにする、と考えるとイメージしやすいでしょう。たとえば「受け取った値をそのまま返す関数」を考えます。具体型で書くと、型ごとに関数が必要になります。
function identityString(arg: string): string {
return arg;
}
function identityNumber(arg: number): number {
return arg;
}
かといってanyで受けると、戻り値の型情報まで失われてしまいます。
function identity(arg: any): any {
return arg;
}
let result = identity("hello"); // result は any(string ではない)
ジェネリクスを使うと、入力の型を覚えたまま、ひとつの定義で済みます。
function identity<T>(arg: T): T {
return arg;
}
let a = identity("hello"); // a は string
let b = identity(42); // b は number
<T>が「これから受け取る型を入れる箱」で、呼び出すたびに実際の型がTに入ります。型の重複をなくしつつ、入力と出力の型のつながりを保てるのがジェネリクス最大の利点です。配列の中身を取り出す関数や、APIの戻り値をラップする関数など、中身の型が呼び出しごとに変わる場面でとくに効きます。
ジェネリクスが解決する問題|anyとの違い
「anyやunknownで十分では?」という疑問はもっともですが、この3つは型情報の扱い方が根本的に異なります。anyは型チェックを実質的に無効化し、unknownは使う前に絞り込みを要求し、ジェネリクスは入力と出力の型を関連づけて保持します。
anyは便利に見えて、誤りを見逃します。次のコードは存在しないプロパティにアクセスしてもエラーになりません。
function wrapAny(arg: any) {
return arg;
}
let v = wrapAny(123);
v.toUpperCase(); // 型エラーにならない → 実行時に壊れる
一方ジェネリクスなら、型が保持されるため補完が効き、誤った操作はコンパイル時に弾かれます。
function wrap<T>(arg: T): T {
return arg;
}
let n = wrap(123);
n.toUpperCase(); // 例:TS2339 Property 'toUpperCase' does not exist on type 'number'.(TypeScriptバージョンにより文言が異なる場合あり)
ジェネリクスでは、引数の型がTに束縛され、そのTを使って戻り値の型が決まります。流れにすると次のとおりです。
引数の型(例:number)→ T に束縛 → 戻り値の型 T が number に決まる
unknownも安全側の型ですが、性質が違います。unknownは「何でも入るが、使う前に必ず型を絞り込む」型で、入力と出力の型を連動させる用途には向きません。戻り値の型を入力の型に連動させたいときがジェネリクスの出番です。
比較表:any・unknown・generics の使い分け
3つの違いを「型情報の保持」「補完の効き」「呼び出し側の安全性」で並べると、選び分けの基準が見えてきます。
| 観点 | any | unknown | generics |
|---|---|---|---|
| 型情報の保持 | 失われる | 具体型は保持しないが安全に不明として扱う | 入力の型を保持する |
| 補完・型チェック | 効かない | 絞り込み後に効く | 効く |
| 呼び出し側の安全性 | 低い(誤りを見逃す) | 高い(絞り込み必須) | 高い(型が連動する) |
| 主な用途 | 型付けを一時的に放棄 | 外部入力を受けて検査 | 型を保ったまま使い回す |
判断の指針はシンプルです。戻り値の型を入力の型に連動させたいならジェネリクス、外部から来る不定の値を受けて検査するならunknownを選びます。anyは型チェックを止めるため、原則として避けるのが安全です。
ジェネリクスの基本構文|型引数Tの書き方
<T>は関数名の直後、引数リストの前に書きます。ここで宣言したTを、引数の型や戻り値の型として使い回せます。
function identity<T>(arg: T): T {
return arg;
}
アロー関数でも考え方は同じで、変数名のあとに型引数を置きます。
const identity = <T>(arg: T): T => arg;
呼び出すときは、型引数を明示する書き方と、省略する書き方があります。
let x = identity<string>("a"); // 明示:T を string に指定
let y = identity("a"); // 省略:引数 "a" から T が string と推論される
どちらでもxとyはstringになり、補完や戻り値の型が効きます。多くの場合は引数から推論されるため、明示は省けます。
なおTはただの名前で、慣習にすぎません。UやKに変えても、Itemのような説明的な名前にしても動作は同じです。読み手に意図が伝わるなら、意味のある名前を付けても構いません。
型引数の命名規則とT以外の使い方
Tが使われるのはType(型)の頭文字という慣習だからで、言語仕様上の決まりではありません。複数の型引数を扱うときには、次のような慣習がよく使われます。
T:Type(最初の型)U:2つ目の型K:Key(キーの型)V:Value(値の型)E:Element(要素の型)
これらはあくまで読みやすさのための目安です。役割が伝わるなら、慣習名より説明的な名前を選んでも問題ありません。オブジェクトのキーを型引数で扱うKのような使い方は、キーの型を取り出す機能と組み合わせると応用が広がります。
複数の型引数を使う
型引数はカンマ区切りで2つ以上宣言できます。それぞれが独立した型を表すため、引数ごとに異なる型を保ったまま扱えます。
function pair<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
const p = pair("id", 42); // p は [string, number]
戻り値をタプルにすると、各要素の型がTとUとして保持されます。オブジェクトで返す形も同様に書けます。
function toEntry<K, V>(key: K, value: V): { key: K; value: V } {
return { key, value };
}
const e = toEntry("age", 30); // e は { key: string; value: number }
型引数を分けることで、2つの値の型が混ざらず独立して保たれる点が複数引数の利点です。
型引数に制約をつける|プロパティアクセスを安全にする
型引数Tの中身に触れたいとき、たとえば.lengthを読みたいときは、Tがそのプロパティを持つことを保証する制約が必要です。extendsを使い、Tが満たすべき形を指定します。
function logLength<T extends { length: number }>(arg: T): number {
return arg.length; // length を持つことが保証される
}
logLength("hello"); // 5
logLength([1, 2, 3]); // 3
まず、制約を付けないTで.lengthに触れると、Tがそのプロパティを持つ保証がないため、アクセスした時点でエラーになります。
function logLengthNg<T>(arg: T): number {
return arg.length; // 例:TS2339 Property 'length' does not exist on type 'T'.(TypeScriptバージョンにより文言が異なる場合あり)
}
一方、extendsで制約を付けた先ほどのlogLengthは.lengthを安全に読めますが、その代わり制約に合わない型を渡すと、今度は呼び出し側で弾かれます。
logLength(123); // 例:TS2345 Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.(TypeScriptバージョンにより文言が異なる場合あり)
extendsという語から、僕も最初は「継承」だと思い込んで戸惑いました。でもこれはTが何かを継承するのではなく、Tが満たすべき形を制約するもの。T extends { length: number }を「lengthを持つ型に限定する条件」と読み替えた瞬間に腹落ちしました。
制約は付ければよいというものではありません。付けすぎると再利用性が下がるため、次の基準で判断します。
| 状況 | 制約 | 理由 |
|---|---|---|
Tのプロパティ・メソッドに触れる |
付ける | 触れる対象の存在を保証するため |
Tをただ受け渡すだけ |
付けない | 制約は不要で、広く使い回せる |
実務ではまず制約なしで書き、プロパティアクセスでエラーが出たときだけextendsを足すのが扱いやすい進め方です。extendsには制約以外に継承や条件型といった使い方もあり、それらを体系的に押さえると応用範囲が広がります。
よくあるエラーと直し方(制約違反)
制約まわりで出るエラーは、「制約そのものを満たさない」か「呼び出し側の型が合わない」かのどちらかが原因です。代表的な2つを切り分けます。
- 制約不一致:渡した型が
extendsで指定した条件を満たさないとき - 引数の型不一致:制約には合うが、引数の値の型が期待と異なるとき
function needsId<T extends { id: number }>(arg: T): number {
return arg.id;
}
needsId({ name: "a" }); // 例:TS2345 Argument of type '{ name: string; }' is not assignable to parameter of type '{ id: number; }'.(TypeScriptバージョンにより文言が異なる場合あり)
直し方は2通りです。制約が厳しすぎるなら緩め、データ側が間違っているなら呼び出し側の型を直します。エラーで握りつぶす型アサーションは安全性を下げるため、原則として避けます。
型推論で型引数を省略する|効く条件・効かない条件
identity<string>のように明示しなくても省略できるのは、引数の値から型引数が推論できるときです。引数にTが現れていれば、渡した値の型からTが決まります。
function identity<T>(arg: T): T {
return arg;
}
let s = identity("hi"); // 引数 "hi" から T = string と推論
このとき、リテラルは一般化(widen)されることがあります。"hi"は"hi"という固定値ではなくstringとして推論されるのが既定の挙動です。
一方、文脈型やデフォルト型引数がない単純な呼び出しでは、引数にTが現れないと推論候補がなく、明示が必要になりやすいです。
function createEmpty<T>(): T[] {
return [];
}
const arr = createEmpty<number>(); // 明示が必要。省略すると T を決められない
推論が効くかどうかは、Tが引数の型に現れるかどうかで判断できます。
Tが引数の型に現れる? |
型引数の決まり方 | 呼び出しの書き方 |
|---|---|---|
| はい | 渡した値の型から推論される | 省略できる |
| いいえ | 推論の手がかりがない | <型>を明示する |
つまり引数からTをたどれるなら省略でき、たどれないなら明示する、と覚えておくと迷いません。条件によって戻り値から型を抽出するような発展的な推論もあります。
クラスとインターフェースでのジェネリクス
ジェネリクスは関数だけでなく、クラス・インターフェース・型エイリアスでも同じように使えます。型引数を定義名の直後に置く点は共通です。
class Box<T> {
constructor(private value: T) {}
get(): T {
return this.value;
}
}
const box = new Box<number>(42);
const n = box.get(); // n は number
インターフェースや型エイリアスでも同様に、中身の型を後から差し込めます。
interface Repository<T> {
findById(id: number): T | undefined;
}
type ApiResult<T> = { ok: boolean; data: T };
型引数にはデフォルト値を指定できます。<T = string>と書くと、指定を省いたときにstringが使われます。
type Container<T = string> = { items: T[] };
const c: Container = { items: ["a", "b"] }; // T は string
型引数のデフォルトを用意しておくと、よく使う型を省略でき、呼び出しが簡潔になります。
標準ライブラリ・ユーティリティ型での実例
ジェネリクスは特別な機能ではなく、日々書いている標準の型の多くがジェネリクスで作られています。たとえば配列やPromise、Mapは型引数を取る形で定義されています。
const nums: Array<number> = [1, 2, 3];
const task: Promise<string> = Promise.resolve("done");
const dict: Map<string, number> = new Map();
Partial<T>のようにオブジェクト型を加工する組み込みの型も、内部はジェネリクスです。自分で<T>を書かない日でも、実際にはジェネリクスの上でコードを書いているわけです。こうした型加工のしくみを体系的に押さえると、設計の引き出しが一気に増えます。
よくある質問
ジェネリクスとanyの違いは何ですか
anyは型情報を捨てて型チェックを止めるため、誤った操作も見逃します。一方ジェネリクスは入力と出力の型を連動させて保持するので、補完が効き、誤りはコンパイル時に弾かれます。型を保ったまま使い回したいならジェネリクスを選びます。
型引数のTは必ずTにしないといけませんか
いいえ、TはType(型)に由来する慣習で、任意の識別子で構いません。Itemのような意味のある名前も使えます。複数あるときはU(2つ目)、K(Key)、V(Value)、E(Element)といった慣習名がよく用いられます。
制約はどんなときに付けますか
型引数Tのプロパティやメソッドに触れたいときに、extendsで制約を付けます。.lengthや.idを読むなら、その存在を保証するためです。逆にただ受け渡すだけなら制約は不要で、付けないほうが広く使い回せます。
型引数を省略できるのはなぜですか
引数の値から型引数を推論できるためです。Tが引数の型に現れていれば、渡した値から自動で決まります。ただし引数に現れない型引数や戻り値だけで決まる型は推論できず、<型>の明示が必要になります。
【付録】さらに学びを深めるためのリソース
さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。
おすすめの書籍
ゼロからわかる TypeScript入門
技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。
変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。
プロを目指す人のためのTypeScript入門
技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。
オンラインで参照できる公式ドキュメント
TypeScript公式ハンドブック
https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。
TypeScript Deep Dive
https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。
TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。
まとめ – 型引数Tと制約extendsを安全に使い回すために
ジェネリクスの要点をまとめます。
- ジェネリクスは型を引数化し、重複なく安全にコードを使い回す仕組み
- 基本構文
<T>から複数引数、extendsによる制約、型推論の順に身につけると理解しやすい Tのプロパティに触れるときだけ制約を付け、付けすぎないのがコツ- 型推論は引数から
Tをたどれるときに効き、たどれないときは明示が必要 anyは型情報を捨てるが、ジェネリクスは入力と出力の型を連動させて保持する
anyに逃げていたコードを、型の安全性を保ったまま書き換えられるようになることが、中級へ進む分岐点です。まず制約なしで書き、必要になったときだけextendsを足すという順番を意識すれば、無理なく使いこなせるようになります。
※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。






