TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - 明示的に型を指定するケース

TypeScript TypeScript
スポンサーリンク

ゴール:「ふだんは推論に任せる。でも“ここだけは型を言語化したい”場面を見抜けるようになる」

ジェネリクスは基本的に「型推論に任せる」のが正解です。
それでも、あえて <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
TypeScript

T = 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);
TypeScript

T を { 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 箇所だけ試してみてください。

そこで、

「推論に任せるところ」と「自分で型を言語化するところ」を
意識的に切り分けられるようになってきたら、
ジェネリクスの“明示指定のセンス”はかなり育ってきています。

タイトルとURLをコピーしました