ゴール:「ふだんは推論に任せる。でも“ここだけは型を言語化したい”場面を見抜けるようになる」
ジェネリクスは基本的に「型推論に任せる」のが正解です。
それでも、あえて <T> を書いた方がいい場面がいくつかあります。
ここでは、
「どんなときに、なぜ明示的に型を指定するのか」
を、具体例ベースで整理していきます。
ケース1:引数からは T を推論できないとき
引数に T が出てこないパターン
典型例はこれです。
function makeEmptyArray<T>(): T[] {
return [];
}
TypeScriptこの関数をこう呼ぶとします。
const xs = makeEmptyArray(); // ここで T は何?
TypeScript引数がないので、TypeScript は T を推論できません。
この場合、T は unknown になったり、エラーになったりします(設定次第)。
ここで初めて、明示的な型指定が必要になります。
const numbers = makeEmptyArray<number>(); // T = number → number[]
const strings = makeEmptyArray<string>(); // T = string → string[]
TypeScriptポイントは、
「T を推論する材料(=引数の型)がないなら、
呼び出し側が <T> を書いてあげるしかない」
ということです。
部分的にしか T が現れないパターン
例えば、こんな関数。
function parseJson<T>(text: string): T {
return JSON.parse(text) as T;
}
TypeScript引数 text からは T を推論できません。
T は「戻り値としてどう扱いたいか」で決まる型だからです。
なので、呼び出し側でこう書きます。
type User = { id: number; name: string };
const user = parseJson<User>('{"id":1,"name":"Taro"}'); // user: User
TypeScriptここでは、
「この JSON を“User として扱いたい”」
という意図を、<User> で明示しています。
このように、
「T が引数に出てこない/出てきても情報が足りない」
ときは、明示的な型指定が必要になりやすいです。
ケース2:推論結果だと“広すぎる/狭すぎる”とき
推論だと広すぎる例
例えば、配列リテラルの推論。
const xs = []; // xs: any[]
xs.push(1);
xs.push("a");
TypeScriptこの状態で、もしジェネリック関数に渡すとします。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const v = first(xs); // T は any → v: any
TypeScriptT = any になってしまい、型安全性が失われます。
ここで、意図をはっきりさせるために型を明示します。
const ys = first<number>([1, 2, 3]); // T = number
TypeScriptあるいは、変数側で型を固定してもいいです。
const xs2: number[] = [];
xs2.push(1);
// first(xs2) → T = number
TypeScript「推論に任せると any や unknown になってしまう」
そんなときは、“自分が本当に欲しい型”を <T> で指定する価値があります。
推論だと狭すぎる例(ユニオンを広げたい)
例えば、こういう関数。
function toArray<T>(value: T): T[] {
return [value];
}
const a = toArray(1); // T = number → number[]
const b = toArray("hello"); // T = string → string[]
TypeScriptここまではいいのですが、
「number | string の配列として扱いたい」ケースを考えます。
const c = toArray(1); // number[]
// ここに string も入れたいが…
TypeScriptこういうとき、最初から T をユニオンで明示してしまう手があります。
const d = toArray<number | string>(1); // (number | string)[]
TypeScriptこのあとで d.push("a") も自然に書けます。
「推論だと単一型になってしまうけど、意図としてはユニオンで扱いたい」
そんなときに、明示的な型指定が効いてきます。
ケース3:制約は満たすが、推論だけでは意図が伝わらないとき
制約付きジェネリクス+明示指定
例えば、こういう関数があります。
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
TypeScriptこれは推論だけで十分に使えます。
getLength("hello"); // T = string
getLength([1, 2, 3]); // T = number[]
TypeScriptただし、「もっと抽象的な型として扱いたい」ときがあります。
const v = { length: 10, extra: "x" };
const len1 = getLength(v); // T = { length: number; extra: string }
TypeScriptここで、あえてこう書くこともできます。
const len2 = getLength<{ length: number }>(v);
TypeScriptT を { length: number } に明示することで、
「この関数にとって重要なのは length だけで、extra はどうでもいい」
という意図を型に落とし込めます。
実務では、
- 「この関数は“このインターフェースとして”扱いたい」
- 「構造的にはもっとプロパティがあるけど、ここでは共通部分だけを見たい」
というときに、制約と同じ形の型を <T> に明示することがあります。
ケース4:API の“使い方”をはっきりさせたいとき
parse 系・変換系の関数
さきほどの parseJson のように、
「戻り値の型は、呼び出し側の意図で決まる」
タイプの関数は、明示的な型指定が前提になりやすいです。
function parseJson<T>(text: string): T {
return JSON.parse(text) as T;
}
type Config = { debug: boolean };
const config = parseJson<Config>('{"debug":true}');
TypeScriptここで <Config> を書くのは、
「この JSON を Config として扱う」
という“契約”を自分で結んでいるイメージです。
TypeScript は中身を検証してくれないので、
「型を信じる代わりに、自分で責任を持つ」という意味も含まれます。
ライブラリ的な関数を設計するとき
自分で汎用的な関数を作るとき、
「ここはユーザーに型を指定してもらう前提だな」と決めることがあります。
例えば、イベントハンドラの登録関数。
type Handler<T> = (value: T) => void;
function onEvent<T>(handler: Handler<T>) {
// ...
}
TypeScript使う側はこう書きます。
onEvent<string>((value) => {
// value: string
});
TypeScriptもちろん、引数から推論させることもできますが、
onEvent((value: string) => {
// ...
});
TypeScriptあえて <string> を書くことで、
「このイベントは string を流すものだ」
という意図を、よりはっきりさせることもあります。
ケース5:推論結果を“固定したい”とき
一度決めた T を、その後も使い回したい
例えば、こんなユーティリティ関数があります。
function createPair<T>(value: T): [T, T] {
return [value, value];
}
TypeScriptこれをこう使うとします。
const p = createPair(1); // [number, number]
TypeScriptここまでは普通ですが、
「この T を別の場所でも使いたい」ことがあります。
type Pair<T> = [T, T];
const p1: Pair<number> = createPair<number>(1);
TypeScriptここで <number> を書くのは、
「Pair<number> と createPair<number> の T を揃えたい」
という意図です。
「型エイリアスや他のジェネリック型と“足並みを揃える”ために、あえて明示する」
という使い方も、実務ではよく出てきます。
まとめ:「明示的に型を指定するケース」を自分の言葉で整理すると
最後に、あなた自身の言葉でこう整理してみてください。
ジェネリクスは基本的に「推論に任せる」のがベース。
でも、次のようなときは <T> を明示した方がいい。
- 引数から T を推論できないとき(
makeEmptyArray<T>(),parseJson<T>()など) - 推論結果だと any/unknown になってしまう、または狭すぎる/広すぎるとき
- 制約は満たすが、「このインターフェースとして扱いたい」という意図を型に乗せたいとき
- API の“使い方”や“契約”を、型としてはっきりさせたいとき
- 他のジェネリック型・型エイリアスと T を揃えたいとき
まずは、自分のコードの中で、
「ここ、TypeScript が推論してくれてるけど、
あえて <T> を書いたら何が変わるかな?」
と 1 箇所だけ試してみてください。
そこで、
「推論に任せるところ」と「自分で型を言語化するところ」を
意識的に切り分けられるようになってきたら、
ジェネリクスの“明示指定のセンス”はかなり育ってきています。
