ゴール:「<T> を書かなくても、勝手に T を決めてくれる感覚をつかむ
ジェネリクスの一番おいしいところのひとつが、「型パラメータをほとんど書かなくていい」ことです。
TypeScript が、関数の引数や戻り値から T を自動で推論してくれるからです。
ここを理解できると、
- 毎回
<number>とか<string>と書かなくてよくなる - 「この場面で T は何になっているか」を頭の中で追えるようになる
ので、ジェネリクスが一気に“実務レベルの道具”になります。
基本:ジェネリック関数の型推論は「引数から T を決める」
identity 関数で推論の基本をつかむ
まずは一番シンプルなジェネリック関数から。
function identity<T>(value: T): T {
return value;
}
const a = identity(1); // a: number
const b = identity("hello"); // b: string
TypeScriptここで大事なのは、<T> を一度も書いていないことです。
本当はこう書けます。
const a = identity<number>(1);
const b = identity<string>("hello");
TypeScriptでも、TypeScript は
valueに渡された引数の型を見て- 「じゃあ T はそれだね」と決めてくれる
ので、 <number> や <string> を省略できています。
つまり、
「ジェネリック関数の T は、基本的に“引数の型から”推論される」
と覚えておくと、かなりスッキリします。
配列版 first 関数でも同じ
もう少しだけ複雑な例。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b", "c"]); // s: string | undefined
TypeScriptここでも <T> は書いていません。
TypeScript は、
arrに渡された[1, 2, 3]の型を見て「T は number」["a", "b", "c"]のときは「T は string」
と自動で決めています。
「T は、引数の型を“逆算”して決まる」
この感覚が、ジェネリクスの推論の基本です。
複数型パラメータの推論:T と U を同時に決める
pair 関数の例
型パラメータが複数あっても、考え方は同じです。
function pair<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
const p1 = pair(1, "a"); // [number, string]
const p2 = pair(true, { x: 1 }); // [boolean, { x: number }]
TypeScriptここで TypeScript は、
aの型から T を推論bの型から U を推論
しています。
pair<number, string>(1, "a") と書いてもいいですが、
ほとんどの場合は推論に任せて問題ありません。
「型パラメータが増えても、基本は“引数から逆算”」
というルールは変わりません。
推論が効くとき・効かないとき
推論が効く典型パターン:「引数に T が現れている」
推論がうまく働くのは、「T が引数の型に現れている」ときです。
function wrap<T>(value: T): { value: T } {
return { value };
}
const w1 = wrap(123); // T = number → { value: number }
const w2 = wrap("hello"); // T = string → { value: string }
TypeScriptvalue: T という引数があるので、
そこに渡された型から T を決められます。
逆に、引数に T が出てこないと推論できません。
推論が効かないパターン:「T が引数に出てこない」
例えば、こんな関数。
function makeEmptyArray<T>(): T[] {
return [];
}
TypeScriptこれをこう呼ぶとどうなるか。
const xs = makeEmptyArray(); // T は何?
TypeScript引数がないので、TypeScript は T を推論できません。
この場合、T は unknown になったり、エラーになったりします(設定による)。
なので、こう書く必要があります。
const xs = makeEmptyArray<number>(); // T = number
TypeScript「T を推論したいなら、T を引数のどこかに“登場させる”必要がある」
これはかなり重要なポイントです。
制約付きジェネリクスと推論
extends があっても「まずは引数から推論」される
制約(extends)があっても、推論の基本は同じです。
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
const a = getLength("hello"); // T = string
const b = getLength([1, 2, 3]); // T = number[]
TypeScriptここで TypeScript は、
valueに渡された型から T を推論- その T が
{ length: number }を満たしているかチェック
という順番で考えています。
もし制約を満たさない型を渡すと、エラーになります。
// getLength(123); // エラー:number は { length: number } を満たさない
TypeScript「推論」と「制約チェック」はセットで動いている
とイメージしておくと理解しやすいです。
複数型パラメータ+制約+推論
もう少しだけレベルを上げてみます。
function getProp<TObj, TKey extends keyof TObj>(
obj: TObj,
key: TKey
): TObj[TKey] {
return obj[key];
}
const user = { id: 1, name: "Taro" };
const id = getProp(user, "id"); // TObj = { id: number; name: string }, TKey = "id"
const name = getProp(user, "name"); // TKey = "name"
// getProp(user, "age"); // エラー:"age" は keyof TObj ではない
TypeScriptここで起きていることは、
objから TObj を推論keyから TKey を推論- TKey が
keyof TObjを満たすかチェック
という流れです。
「推論 → 制約チェック → 戻り値の型決定」
この順番を意識できると、かなり読み解きやすくなります。
デフォルト型パラメータと推論の関係
「推論できなかったとき」にデフォルトが効く
デフォルト型パラメータと推論は、こういう関係です。
function logValue<T = string>(value: T): void {
console.log(value);
}
TypeScriptここでのルールは、
- まず引数から T を推論しようとする
- 推論できたら、その型を使う
- 推論できなかったら、デフォルト(ここでは string)を使う
例えば:
logValue("hello"); // T は string(推論)
logValue<number>(123); // T は number(明示)
TypeScriptこの関数は引数に T が現れているので、
ほぼ常に推論で決まります。
デフォルトが効くのは、T を明示して呼び出すような特殊なケースか、
引数に T が現れないパターンです。
「推論が最優先、デフォルトは“最後の保険”」
と覚えておくと整理しやすいです。
実務で意識したい「推論とうまく付き合うコツ」
コツ1:T を“ちゃんと引数に登場させる”
推論を効かせたいなら、T を引数のどこかに必ず出すことです。
// 推論しやすい
function wrap<T>(value: T): { value: T } {
return { value };
}
// 推論しづらい(引数に T がない)
function makeArray<T>(): T[] {
return [];
}
TypeScript後者のような関数は、
「毎回 <number> などを明示する前提」で設計するか、
そもそも API の形を見直した方がいいことが多いです。
コツ2:推論に任せて、明示は“例外的なときだけ”
ジェネリクスを書き始めたばかりだと、
つい全部に <number> や <string> を書きたくなります。
でも、基本は「推論に任せる」で OK です。
明示した方がいいのは、例えばこんなときです。
- 引数からは T が決められない(
makeEmptyArray<T>()など) - 推論された型だと広すぎる/狭すぎるので、意図をはっきりさせたい
それ以外は、どんどん省略していきましょう。
その方がコードも読みやすくなります。
まとめ:関数とジェネリクスの推論を自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
ジェネリック関数の型パラメータ T は、
「基本的に、引数の型から逆算して自動で決まる」。
複数型パラメータでも同じで、
それぞれが対応する引数から推論される。
制約(extends)は、
「推論された T がこの条件を満たしているか」をチェックするフィルター。
デフォルト型パラメータは、
「推論できなかったときにだけ使われる“型の初期値”」。
まずは次の 2 つを、自分の手で書いて試してみてください。
function identity<T>(value: T): T { return value; }
function getProp<TObj, TKey extends keyof TObj>(obj: TObj, key: TKey): TObj[TKey] { return obj[key]; }
TypeScriptそして、<T> を書いた場合と書かない場合、
どんな型が推論されるかをエディタで眺めてみてください。
そこで、
「T は自分で全部書くんじゃなくて、“TypeScript に決めさせる”のが基本なんだな」
と感じられたら、
関数とジェネリクスの推論はもうしっかり掴めています。
