TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - 関数とジェネリクスの推論

TypeScript TypeScript
スポンサーリンク

ゴール:「<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 }
TypeScript

value: 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

ここで起きていることは、

  1. obj から TObj を推論
  2. key から TKey を推論
  3. TKey が keyof TObj を満たすかチェック

という流れです。

「推論 → 制約チェック → 戻り値の型決定」
この順番を意識できると、かなり読み解きやすくなります。

デフォルト型パラメータと推論の関係

「推論できなかったとき」にデフォルトが効く

デフォルト型パラメータと推論は、こういう関係です。

function logValue<T = string>(value: T): void {
  console.log(value);
}
TypeScript

ここでのルールは、

  1. まず引数から T を推論しようとする
  2. 推論できたら、その型を使う
  3. 推論できなかったら、デフォルト(ここでは 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 に決めさせる”のが基本なんだな」

と感じられたら、
関数とジェネリクスの推論はもうしっかり掴めています。

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