TypeScript型推論完全ガイド〜contextual typing・ジェネリクス推論・落とし穴・NoInfer【2026年版】〜

TypeScriptを書いていて、「なぜここはこの型に推論されるのか」「明示しないと壊れるのはなぜか」がモヤッとしたまま手癖でコードを書いている人は多いはずです。型推論はTypeScriptの最大の生産性ブースターであり、同時に最大のハマりポイントでもあります。本記事ではTypeScript 5.x準拠で、変数・関数・ジェネリクス・as constsatisfiesinferNoInferまで、推論の仕組みを40以上のコピペで動くコード例と共に解体します。

対象は20〜40代の現役Webエンジニアで、TypeScript型の基礎ジェネリクス完全ガイドを読了し、「推論が思い通りにならないときに自分で原因特定できるレベル」を目指す層です。読了後はas constsatisfiesの使い分け、NoInfer(TS 5.4+)の活用、Conditional Type内のinferパターンを「型がhoverで何になるか先に脳内で当てられる」解像度まで到達できます。

  1. 型推論とは何か:TypeScriptが代わりに書いてくれる型注釈
    1. 推論が効く3つの場面
    2. 推論の威力:DRYと型安全の両立
    3. 本記事で扱う推論パターン全体像
  2. 変数宣言の推論:let と const の決定的な違い
    1. プリミティブ値での推論差
    2. オブジェクトプロパティは const でも widening される
    3. null / undefined の推論
  3. 配列リテラルの推論:union と tuple の境界
    1. 基本: union 配列としての推論
    2. as const でタプル化
    3. タプルを明示的に型注釈する
  4. オブジェクトリテラルの推論:Excess Property と Fresh Object
    1. 余剰プロパティチェックの基本
    2. Fresh Object Literal の判定
    3. オプショナルと余剰の組み合わせ
  5. as const と satisfies の正しい使い分け
    1. 3パターンの比較
    2. satisfies が真価を発揮するケース
    3. as const と satisfies の組み合わせ
  6. 関数の戻り値推論と contextual typing
    1. 戻り値推論の基本
    2. contextual typing で引数型を省略
    3. contextual typing が効かないパターン
  7. ジェネリクスの型引数推論
    1. 基本的な型引数推論
    2. 推論が失敗するケースと明示
    3. extends constraint で推論を誘導する
    4. NoInfer による推論抑制 (TS 5.4+)
  8. Conditional Type と infer による推論
    1. infer の基本構文
    2. Awaited:再帰的 Promise 解決
    3. tuple から head / tail / last を取り出す
    4. 分配条件型(distributive)と infer
  9. 配列メソッドの推論:map / filter / reduce / fromEntries
    1. map の推論
    2. filter による絞り込みと型ガード関数
    3. reduce の推論失敗パターン
    4. Object.fromEntries の推論限界
  10. narrowing:推論を分岐で絞り込む
    1. typeof による narrowing
    2. discriminated union と narrowing
    3. narrowing が消える落とし穴
  11. const enum / enum と型推論
    1. enum / const enum / union 比較
  12. 共変・反変(Variance)と関数代入
    1. 共変・反変の基本
    2. 配列メソッドのコールバック型と Variance
  13. キャスト(as)と型アサーションの正しい使い方
    1. as の正しいユースケース
    2. satisfies で as を置き換える
  14. 推論結果を確認する実践テクニック
    1. VSCode で型を確認する3つの方法
    2. tsc / tsserver で型を吐く
  15. パフォーマンス影響:推論コストと型コンプレキシティ
    1. 推論を重くするパターン
    2. 計測ツール
  16. Before / After:推論を活かしたリファクタリング集
    1. 例1:設定オブジェクトの二重定義
    2. 例2:API レスポンスの型
    3. 例3:Reducer の型
    4. 例4:状態マシンの型安全化
  17. よくある推論失敗とトラブルシュート
    1. Q1: any にされてしまう
    2. Q2: union に意図せず潰される
    3. Q3: 推論結果が想定より広い
    4. Q4: filter で型が絞れない
    5. Q5: ジェネリクスの型引数が unknown になる
  18. キャリアアップ:TypeScript の型推論を武器にする
    1. 体系学習で抜け漏れを潰す
    2. 転職で「TypeScript型に強い」を武器にする
    3. 未経験から本気でモダンフロントを習得するなら
    4. すでに現役なら、より高単価の現場へ
  19. まとめ:推論を味方につけるための7原則

型推論とは何か:TypeScriptが代わりに書いてくれる型注釈

型推論(Type Inference)とは、明示的な型注釈を書かなくてもTypeScriptが文脈から型を自動的に決定する仕組みです。const x = 1; と書けばx1型(リテラル型)、let x = 1; と書けばxnumber型になります。この一行の挙動の差に、推論アルゴリズムの本質が詰まっています。

推論が効く3つの場面

// (1) 変数宣言での初期値推論
const a = 42;        // a: 42 (リテラル型)
let   b = 42;        // b: number (widening)

// (2) 関数の戻り値推論
function double(n: number) {
  return n * 2;      // 戻り値: number と推論
}
type R = ReturnType<typeof double>; // number

// (3) ジェネリクスの型引数推論
function first<T>(arr: T[]): T {
  return arr[0];
}
const v = first([1, 2, 3]); // T = number と推論

推論の威力:DRYと型安全の両立

「型注釈は冗長に書くべき」と教わった人ほど、推論を信頼しないコードを書きがちです。しかし推論できる場所では推論に任せた方が、型と実装の整合性が崩れにくいのがTypeScriptの設計思想です。

// ❌ 冗長な型注釈(値と二重管理)
const user: { id: number; name: string } = { id: 1, name: "alice" };

// ✅ 推論に任せる(値の変更が型に追従)
const user = { id: 1, name: "alice" };
//    ^? const user: { id: number; name: string }

// ❌ 戻り値型を二重に書く
function getUser(): { id: number; name: string } {
  return { id: 1, name: "alice" };
}

// ✅ 戻り値は推論に任せて、引数だけ明示
function getUser() {
  return { id: 1, name: "alice" };
}
type User = ReturnType<typeof getUser>; // 単一の真実の源

本記事で扱う推論パターン全体像

カテゴリ主なトピック難易度
変数let/const・widening・narrowing★☆☆
リテラル配列・オブジェクト・as const・satisfies★★☆
関数戻り値推論・contextual typing★★☆
ジェネリクス型引数推論・推論失敗・NoInfer★★★
Conditionalinfer・Awaited・分配★★★
配列メソッドmap/filter/reduce/fromEntries★★☆
落とし穴Excess Property・Fresh Object・Variance★★★

変数宣言の推論:let と const の決定的な違い

最も基本的でかつ最も見落とされがちなのが、letconstでの推論結果の違いです。constは再代入できないため、初期値のリテラル型がそのまま型になります(narrow)。letは再代入可能なため、より広い型(widening)に拡張されます。

プリミティブ値での推論差

// const = リテラル型(narrow)
const n1 = 42;        // 42
const s1 = "hello";   // "hello"
const b1 = true;      // true

// let = プリミティブ型(widening)
let n2 = 42;          // number
let s2 = "hello";     // string
let b2 = true;        // boolean

// 関数引数として渡したときの差
function takeLiteral(x: 42) {}
takeLiteral(n1); // ✅ OK
takeLiteral(n2); // ❌ Argument of type 'number' is not assignable to '42'

オブジェクトプロパティは const でも widening される

意外な落とし穴が、オブジェクトのプロパティはconstでもwideningされることです。プロパティは内部的に書き換え可能だからです。

// const でもプロパティは widening される
const obj = { kind: "circle", radius: 5 };
//    ^? const obj: { kind: string; radius: number }
//                          ^^^^^^ "circle" にならない!

// これだと困るケース
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(s: Shape) { /* ... */ }
area(obj); // ❌ string は "circle" | "square" に代入できない

// 解決策1: as const
const obj1 = { kind: "circle", radius: 5 } as const;
//    ^? const obj1: { readonly kind: "circle"; readonly radius: 5 }

// 解決策2: satisfies
const obj2 = { kind: "circle", radius: 5 } satisfies Shape;
//    ^? const obj2: { kind: "circle"; radius: number }

// 解決策3: 型注釈
const obj3: Shape = { kind: "circle", radius: 5 };

null / undefined の推論

// strictNullChecks=true 前提

// let で null/undefined 初期化は any (strict外) または該当型
let x1 = null;        // null (strict)
let x2 = undefined;   // undefined (strict)

// 後から再代入したい場合は型注釈が必須
let user: User | null = null;
user = { id: 1, name: "alice" }; // ✅

// const は当然そのまま
const z1 = null;      // null
const z2 = undefined; // undefined

配列リテラルの推論:union と tuple の境界

配列リテラルは「要素型のunion で構成された配列型」として推論されます。タプル型にしたい場合はas constまたは明示的なタプル型注釈が必要です。

基本: union 配列としての推論

// 同じ型なら素直
const nums = [1, 2, 3];        // number[]
const strs = ["a", "b", "c"];  // string[]

// 異なる型が混ざると union 配列
const mixed = [1, "a", true];  // (string | number | boolean)[]

// tuple ではない点に注意
const pair = [1, "alice"];     // (string | number)[]
//    ^? NOT [number, string]
const [id, name] = pair;
//     ^? number | string  ← name に id の型まで混入

as const でタプル化

// as const で readonly tuple に固定
const pair = [1, "alice"] as const;
//    ^? readonly [1, "alice"]

const [id, name] = pair;
//     ^? id: 1, name: "alice"

// ReactのuseState風の戻り値もこの仕組み
function useToggle(init: boolean) {
  let v = init;
  const toggle = () => { v = !v; };
  return [v, toggle] as const;
  //     ^? readonly [boolean, () => void]
}

const [value, toggle] = useToggle(false);
//     ^? boolean, () => void  ← 分割代入で型が崩れない

タプルを明示的に型注釈する

// 明示型注釈でタプル化
const pair: [number, string] = [1, "alice"];

// 戻り値型でタプル指定
function divmod(a: number, b: number): [number, number] {
  return [Math.floor(a / b), a % b];
}
const [q, r] = divmod(10, 3); // q: number, r: number

// 可変長タプル(TS 4.0+)
type Range = [start: number, end: number, ...steps: number[]];
const r: Range = [0, 10, 1, 2, 5];

オブジェクトリテラルの推論:Excess Property と Fresh Object

オブジェクトリテラルにはExcess Property Check(余剰プロパティチェック)という特殊なルールがあり、これは「リテラルそのものを直接代入したときだけ厳しくなる」一見不思議な挙動を生みます。理解せずに使うと「同じオブジェクトなのに変数経由ならOKでリテラルならエラー」というハマりに遭遇します。

余剰プロパティチェックの基本

type User = { id: number; name: string };

// ❌ オブジェクトリテラル直接代入は厳しくチェックされる
const u1: User = { id: 1, name: "alice", age: 30 };
//                                       ^^^ Object literal may only specify known properties

// ✅ 一度変数経由ならOK(Fresh ではないので緩い)
const tmp = { id: 1, name: "alice", age: 30 };
const u2: User = tmp; // OK(age は無視されるだけ)

// この挙動の理由:タイポ検出を優先したい
const u3: User = { id: 1, naem: "alice" };
//                         ^^^^ "name" のタイポ検出

Fresh Object Literal の判定

// "Fresh" とは「リテラルとして生まれてから一度も別の場所に代入されていない」状態

function takeUser(u: User) {}

// ✅ Fresh だが、関数呼び出しの引数は Excess Check 対象
takeUser({ id: 1, name: "alice", age: 30 });
//                                ^^^ Excess Property エラー

// ✅ 一度束縛したオブジェクトは Fresh ではなくなる
const obj = { id: 1, name: "alice", age: 30 };
takeUser(obj); // OK

// ✅ as キャストで Fresh を剥がす(非推奨だが知っておく)
takeUser({ id: 1, name: "alice", age: 30 } as User); // OK だが危険

オプショナルと余剰の組み合わせ

type Config = {
  host: string;
  port?: number;
  // index signature がない
};

// ❌ オプショナルにない未知プロパティはNG
const c1: Config = { host: "x", potr: 80 };
//                              ^^^^ "port" のタイポ警告

// ✅ index signature を足すと許容になる
type ConfigLoose = {
  host: string;
  port?: number;
  [key: string]: unknown;
};
const c2: ConfigLoose = { host: "x", debug: true }; // OK

as const と satisfies の正しい使い分け

TS 4.9で追加されたsatisfiesは、as constと「型注釈」の中間の役割を担います。3者の違いを理解せずに混在させると、不要なreadonlyや型情報の喪失を招きます。

3パターンの比較

type Color = "red" | "green" | "blue";
type Palette = Record<string, Color>;

// (1) 型注釈:右辺の型が左辺の型に上書きされる
const p1: Palette = {
  primary: "red",
  accent: "blue",
};
p1.primary; // Color (具体名が消える)

// (2) as const:値そのものの最narrow型に固定
const p2 = {
  primary: "red",
  accent: "blue",
} as const;
p2.primary; // "red"  ← 具体値が残る
// ただし Palette を満たすかはチェックされない

// (3) satisfies:型をチェックしつつ、推論結果を保持
const p3 = {
  primary: "red",
  accent: "blue",
} satisfies Palette;
p3.primary; // "red"  ← 具体値が残る かつ Palette 違反は型エラー

satisfies が真価を発揮するケース

type RouteConfig = Record<string, { path: string; auth?: boolean }>;

// ❌ 型注釈だと具体的なキー名が string に潰れる
const routesA: RouteConfig = {
  home: { path: "/" },
  admin: { path: "/admin", auth: true },
};
type KeysA = keyof typeof routesA; // string ← 個別キーが取れない

// ✅ satisfies なら具体的キー名が保持される
const routesB = {
  home: { path: "/" },
  admin: { path: "/admin", auth: true },
} satisfies RouteConfig;
type KeysB = keyof typeof routesB; // "home" | "admin"
const k: KeysB = "home"; // OK

as const と satisfies の組み合わせ

// 両方付けることで「readonly + 制約チェック + narrow型保持」を実現

type EventMap = Record<string, { payload: unknown }>;

const events = {
  "user.created": { payload: { id: 0, name: "" } },
  "user.deleted": { payload: { id: 0 } },
} as const satisfies EventMap;

type EventName = keyof typeof events;
// "user.created" | "user.deleted"

type CreatedPayload = typeof events["user.created"]["payload"];
// { readonly id: 0; readonly name: "" }
// 具体的構造まで型として保持される

関数の戻り値推論と contextual typing

関数の引数は明示するが戻り値は推論に任せる、というのがTypeScriptの一般的な書き方です。さらに関数式やコールバックでは、外側の文脈から引数型が推論されるcontextual typingが働きます。

戻り値推論の基本

// 戻り値は条件分岐をすべて辿って union として推論される
function classify(n: number) {
  if (n < 0) return "negative" as const;
  if (n === 0) return "zero" as const;
  return "positive" as const;
}
type C = ReturnType<typeof classify>;
// "negative" | "zero" | "positive"

// 早期リターンと throw は型から除外される
function getOrThrow(map: Map<string, number>, key: string) {
  const v = map.get(key);
  if (v === undefined) throw new Error("not found");
  return v; // number(undefined は除外済み)
}

contextual typing で引数型を省略

// 配列メソッドのコールバックは contextual typing が効く
const nums = [1, 2, 3];

nums.forEach((n) => {
  //         ^? n: number  ← 引数型を書かなくても推論
  console.log(n.toFixed(2));
});

nums.map((n, i) => n * i);
// n: number, i: number, 戻り値: number → map の戻り値: number[]

// addEventListener のイベント型も contextual typing
document.addEventListener("click", (e) => {
  //                                ^? e: MouseEvent
  console.log(e.clientX);
});

// Promise.then も同様
fetch("/api").then((res) => {
  //               ^? res: Response
  return res.json();
});

contextual typing が効かないパターン

// ❌ 別変数に切り出すと contextual typing が消える
const handler = (e) => {
  //            ^^^ Parameter 'e' implicitly has an 'any' type
  console.log(e.clientX);
};
document.addEventListener("click", handler);

// ✅ 解決策1: 型注釈
const handler1 = (e: MouseEvent) => {
  console.log(e.clientX);
};

// ✅ 解決策2: 型エイリアスで関数型を定義
type Handler = (e: MouseEvent) => void;
const handler2: Handler = (e) => {
  console.log(e.clientX); // OK
};

// ✅ 解決策3: インラインで書く
document.addEventListener("click", (e) => {
  console.log(e.clientX); // OK
});

ジェネリクスの型引数推論

ジェネリクス関数は引数の型から自動的に型引数を決定します。これが効かないとき、または意図しない型に推論されたときの対処を理解することが、ライブラリ実装で必要になります。

基本的な型引数推論

function identity<T>(x: T): T {
  return x;
}

const a = identity(42);          // T = number → a: number
const b = identity("hi");        // T = string → b: string
const c = identity<42>(42);      // 明示も可能 → c: 42

// 配列要素の推論
function head<T>(arr: T[]): T | undefined {
  return arr[0];
}
const h1 = head([1, 2, 3]);      // T = number → h1: number | undefined
const h2 = head(["a", "b"]);     // T = string → h2: string | undefined

// 複数引数からの推論
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}
const p = pair(1, "x");          // A = number, B = string → [number, string]

推論が失敗するケースと明示

// 戻り値だけに登場する型引数は推論されない
function create<T>(): T {
  return {} as T;
}
const x = create();         // T が決まらない → unknown(または契約上の型)
const y = create<User>();   // 明示が必要

// オーバーロード解決と推論
function parse<T>(input: string): T;
function parse<T>(input: string, fallback: T): T;
function parse<T>(input: string, fallback?: T): T {
  try { return JSON.parse(input); } catch { return fallback as T; }
}

const v1 = parse("{}");                 // T 不明 → 明示推奨
const v2 = parse<User>("{...}");        // T = User
const v3 = parse("{}", { id: 0 });      // fallback から T = { id: number }

extends constraint で推論を誘導する

// keyof で推論を絞る
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "alice", age: 30 };
const id = pluck(user, "id");     // K = "id" → id: number
const name = pluck(user, "name"); // K = "name" → name: string
// const x = pluck(user, "xxx"); // ❌ "xxx" は keyof user に含まれない

// 配列要素の型を返す
function last<T extends readonly unknown[]>(arr: T): T[number] {
  return arr[arr.length - 1];
}
const e = last([1, "a", true] as const);
//    ^? 1 | "a" | true

NoInfer による推論抑制 (TS 5.4+)

TS 5.4で追加されたNoInfer<T>は、「この引数からは型推論しないでほしい」と指示するユーティリティ型です。複数引数からの推論競合を避けたいときに使います。

// ❌ 第2引数からも T が推論されて緩くなる
function createState<T>(initial: T, fallback: T): T {
  return Math.random() > 0.5 ? initial : fallback;
}
const s1 = createState("a", "b");      // T = "a" | "b"(union 化)
const s2 = createState(1 as const, 2); // T = 1 | 2

// ✅ NoInfer で第1引数のみから推論
function createStateV2<T>(initial: T, fallback: NoInfer<T>): T {
  return Math.random() > 0.5 ? initial : fallback;
}
const s3 = createStateV2("a", "b");
// T = "a"(initial だけから推論)
// "b" は "a" に代入できないので型エラー → 意図しない union を防げる

// 設定オブジェクトのバリデーションでも有効
type Config<T extends string> = {
  options: T[];
  default: NoInfer<T>;
};

const cfg: Config<"a" | "b"> = {
  options: ["a", "b"],
  default: "a", // OK
  // default: "c", // ❌ "a" | "b" のみ
};

Conditional Type と infer による推論

inferキーワードは、Conditional Type内で「型の一部を変数として取り出す」仕組みです。ReturnTypeAwaitedなど、組み込みユーティリティ型の中核を成します。

infer の基本構文

// 関数型から戻り値を取り出す
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() { return { id: 1, name: "alice" }; }
type U = MyReturnType<typeof getUser>;
// { id: number; name: string }

// 配列の要素型を取り出す
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<number[]>;      // number
type E2 = ElementOf<("a" | "b")[]>; // "a" | "b"

// Promise の中身を取り出す(Awaited の簡易版)
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type P1 = UnwrapPromise<Promise<number>>; // number
type P2 = UnwrapPromise<string>;            // string(Promiseでなければそのまま)

Awaited:再帰的 Promise 解決

// 標準の Awaited は Promise の入れ子も再帰的に剥がす
type A1 = Awaited<Promise<number>>;                  // number
type A2 = Awaited<Promise<Promise<string>>>;          // string(再帰解決)
type A3 = Awaited<Promise<Promise<Promise<User>>>>;   // User

// async 関数の戻り値推論で活躍
async function fetchUser(id: number) {
  const res = await fetch(`/users/${id}`);
  return res.json() as Promise<User>;
}
type FetchUserResult = Awaited<ReturnType<typeof fetchUser>>;
// User(Promise が剥がれる)

// 自前で Awaited 風に書く場合(参考実装)
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;

tuple から head / tail / last を取り出す

type Head<T extends readonly unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends [unknown, ...infer R] ? R : [];
type Last<T extends readonly unknown[]> = T extends [...unknown[], infer L] ? L : never;

type T1 = Head<[1, 2, 3]>;  // 1
type T2 = Tail<[1, 2, 3]>;  // [2, 3]
type T3 = Last<[1, 2, 3]>;  // 3

// 関数引数列の操作
type Parameters2<F> = F extends (...args: infer A) => unknown ? A : never;
type DropFirst<F> = F extends (a: any, ...rest: infer R) => unknown ? R : never;

function send(channel: string, ...payload: number[]) {}
type SendArgs = Parameters2<typeof send>;     // [string, ...number[]]
type SendRest = DropFirst<typeof send>;       // number[]

分配条件型(distributive)と infer

// union に対して Conditional Type は分配される
type ToArray<T> = T extends unknown ? T[] : never;
type R1 = ToArray<string | number>;
// string[] | number[]  ← 分配される

// 分配を抑制したいときは [] で囲む
type ToArrayNoDist<T> = [T] extends [unknown] ? T[] : never;
type R2 = ToArrayNoDist<string | number>;
// (string | number)[]

// 実用例: union から特定の型だけ取り出す
type ExtractFunc<T> = T extends (...args: any[]) => any ? T : never;
type Mix = string | (() => void) | number | ((x: number) => string);
type Funcs = ExtractFunc<Mix>;
// (() => void) | ((x: number) => string)

配列メソッドの推論:map / filter / reduce / fromEntries

日常的に使う配列メソッドは、「推論が綺麗に効くケース」と「明示しないと型が崩れるケース」がはっきり分かれます。特にfilterreduceは要注意です。

map の推論

const nums = [1, 2, 3];

// シンプルな変換は綺麗に推論される
const doubled = nums.map((n) => n * 2);
//    ^? number[]

const strs = nums.map((n) => n.toString());
//    ^? string[]

// オブジェクトへの変換
const items = nums.map((n) => ({ id: n, label: `Item ${n}` }));
//    ^? { id: number; label: string }[]

// インデックス活用
const enumerated = nums.map((n, i) => [i, n] as const);
//    ^? (readonly [number, number])[]

filter による絞り込みと型ガード関数

// ❌ 通常の filter は型を絞らない
const xs: (number | null)[] = [1, null, 2, null, 3];
const onlyNums = xs.filter((x) => x !== null);
//    ^? (number | null)[]  ← null が残ったまま!

// ✅ 型述語(type predicate)で絞る
const onlyNumsV2 = xs.filter((x): x is number => x !== null);
//    ^? number[]

// ✅ TS 5.5+ では `filter` の戻り値型を自動推論する強化が入る
const onlyNumsV3 = xs.filter((x) => x !== null);
// TS 5.5+: number[]

// 汎用 NonNullable ガード
function isNonNull<T>(x: T): x is NonNullable<T> {
  return x !== null && x !== undefined;
}
const onlyNumsV4 = xs.filter(isNonNull);
//    ^? number[]

reduce の推論失敗パターン

// ❌ 初期値が空配列だと推論失敗
const items = [
  { tag: "a", value: 1 },
  { tag: "b", value: 2 },
  { tag: "a", value: 3 },
];

const grouped1 = items.reduce((acc, item) => {
  acc[item.tag] = acc[item.tag] || [];
  //  ^^^^^^^^ never に index アクセスはできない
  acc[item.tag].push(item);
  return acc;
}, {});

// ✅ 初期値に型注釈
const grouped2 = items.reduce<Record<string, typeof items>>((acc, item) => {
  acc[item.tag] = acc[item.tag] || [];
  acc[item.tag].push(item);
  return acc;
}, {});

// ✅ もしくは初期値を明示型
const init: Record<string, typeof items> = {};
const grouped3 = items.reduce((acc, item) => {
  acc[item.tag] = acc[item.tag] || [];
  acc[item.tag].push(item);
  return acc;
}, init);

Object.fromEntries の推論限界

// ❌ 標準の Object.fromEntries は string キーに潰れる
const entries = [["a", 1], ["b", 2]] as const;
const obj1 = Object.fromEntries(entries);
//    ^? { [k: string]: 1 | 2 }  ← 具体キー不明

// ✅ 自前で型安全 fromEntries を書く
function fromEntries<
  const T extends readonly (readonly [PropertyKey, unknown])[]
>(entries: T): { [K in T[number] as K[0]]: K[1] } {
  return Object.fromEntries(entries) as any;
}

const obj2 = fromEntries([["a", 1], ["b", 2]] as const);
//    ^? { a: 1; b: 2 }

// 実用: 設定キーの一覧から型安全な辞書を生成
const KEYS = ["host", "port", "debug"] as const;
const config = fromEntries(KEYS.map((k) => [k, ""] as const));
//    ^? { host: ""; port: ""; debug: "" }

narrowing:推論を分岐で絞り込む

narrowing(型の絞り込み)は「ある変数の型を、分岐内でより具体的な型に推論し直す」仕組みです。typeof / instanceof / in 演算子 / === / 型述語など、多数の手段があります。

typeof による narrowing

function format(v: string | number | boolean) {
  if (typeof v === "string") {
    return v.toUpperCase();  // v: string
  }
  if (typeof v === "number") {
    return v.toFixed(2);     // v: number
  }
  return v ? "Y" : "N";       // v: boolean(残り)
}

discriminated union と narrowing

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rect"; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;       // s.radius が見える
    case "square": return s.side * s.side;                // s.side が見える
    case "rect":   return s.w * s.h;                       // s.w, s.h が見える
  }
}

// exhaustiveness check
function areaStrict(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "square": return s.side * s.side;
    case "rect":   return s.w * s.h;
    default:
      const _exhaustive: never = s; // 全分岐網羅されていなければ型エラー
      return _exhaustive;
  }
}

narrowing が消える落とし穴

// ❌ クロージャ内では narrowing が失われる
function buggy(input: string | null) {
  if (input === null) return;
  // ここでは input: string

  setTimeout(() => {
    input.toUpperCase(); // ❌ input は string | null に戻る
    //    ^^^^^^^^^ Object is possibly 'null'
  }, 100);
}

// ✅ ローカル変数に束縛して narrowing を保持
function fixed(input: string | null) {
  if (input === null) return;
  const s = input; // s: string
  setTimeout(() => {
    s.toUpperCase(); // OK
  }, 100);
}

const enum / enum と型推論

enumは推論の文脈で挙動が独特で、「union of literal types」と等価ではなく、専用の名義型(nominal type)として振る舞います。リテラル型とas constオブジェクトの方が推論との相性は良いケースが多いです。

enum / const enum / union 比較

// (1) 通常の enum:ランタイムオブジェクトが生成される
enum ColorA { Red = "red", Green = "green" }
function takeA(c: ColorA) {}
takeA("red");        // ❌ 文字列リテラルは enum と互換でない
takeA(ColorA.Red);   // ✅

// (2) const enum:インライン化される(配布ライブラリでは非推奨)
const enum ColorB { Red = "red", Green = "green" }
function takeB(c: ColorB) {}
takeB(ColorB.Red);   // ✅(コンパイル後は "red" にインライン)

// (3) as const オブジェクト + union(推奨)
const ColorC = { Red: "red", Green: "green" } as const;
type ColorC = typeof ColorC[keyof typeof ColorC]; // "red" | "green"
function takeC(c: ColorC) {}
takeC("red");          // ✅
takeC(ColorC.Red);     // ✅

共変・反変(Variance)と関数代入

関数型の代入可能性には「引数は反変・戻り値は共変」というルールがあり、これが推論結果の互換性に直結します。特にコールバック引数が想定より広い型に推論される現象の元凶です。

共変・反変の基本

// 引数: 反変(広い方を受け取れる関数は、狭い方を要求する場所に代入可能)
type AnimalHandler = (a: { name: string }) => void;
type DogHandler    = (d: { name: string; breed: string }) => void;

const animalH: AnimalHandler = (a) => console.log(a.name);
const dogH: DogHandler = animalH;   // ✅ AnimalHandler は DogHandler に代入可
// (DogHandler の場所は Animal を受ければ十分なので OK)

// 戻り値: 共変(狭い方を返す関数は、広い方を要求する場所に代入可能)
type GetAnimal = () => { name: string };
type GetDog    = () => { name: string; breed: string };

const getDog: GetDog = () => ({ name: "pochi", breed: "shiba" });
const getAnimal: GetAnimal = getDog; // ✅ Dog は Animal を満たす

// strictFunctionTypes=true で引数は厳密な反変チェック

配列メソッドのコールバック型と Variance

// Array.prototype.forEach の callback は (v, i, arr) => void
// 引数を一部だけ書いても OK なのは反変ゆえ
const xs = [1, 2, 3];

xs.forEach(() => {});            // ✅ 引数なし
xs.forEach((v) => {});           // ✅
xs.forEach((v, i) => {});        // ✅
xs.forEach((v, i, arr) => {});   // ✅

// 戻り値型の弛緩例
xs.forEach((v) => v.toString()); // ✅ 戻り値 void が要求でも string を返してOK

キャスト(as)と型アサーションの正しい使い方

キャスト(as)は「推論結果を強制的に上書きする」最終手段です。多用は型安全性を破壊しますが、適切に使えば外部入力との接続点で必要になります。

as の正しいユースケース

// (1) JSON のレスポンスを型付け(検証は別途すべき)
const res = await fetch("/api/user");
const user = await res.json() as User;

// (2) DOM 要素のキャスト
const el = document.getElementById("app") as HTMLDivElement;

// (3) 段階的な any → 具体型
const data = JSON.parse("{}") as unknown as Config;

// ❌ ダメな例:推論結果を雑に書き換える
const n = "42" as unknown as number; // 実行時は string のまま!

// ✅ ランタイム検証と組み合わせる
function asUser(x: unknown): User {
  if (typeof x !== "object" || x === null) throw new Error();
  if (typeof (x as any).id !== "number") throw new Error();
  if (typeof (x as any).name !== "string") throw new Error();
  return x as User;
}

satisfies で as を置き換える

type Theme = Record<string, { bg: string; fg: string }>;

// ❌ as Theme だと具体的なキーが消える
const themeA = {
  light: { bg: "#fff", fg: "#000" },
  dark: { bg: "#000", fg: "#fff" },
} as Theme;
themeA.light;  // { bg: string; fg: string }
themeA.xxxx;   // 型エラーにならない(string indexable)

// ✅ satisfies なら具体的構造と制約の両方を維持
const themeB = {
  light: { bg: "#fff", fg: "#000" },
  dark: { bg: "#000", fg: "#fff" },
} satisfies Theme;
themeB.light;  // { bg: string; fg: string }
themeB.xxxx;   // ❌ 型エラー

推論結果を確認する実践テクニック

推論結果を頭の中だけで考えるのは限界があります。エディタとTypeScript本体の機能を駆使して、推論結果を可視化するのがプロの作業です。

VSCode で型を確認する3つの方法

// (1) hover:変数や式にカーソルを当てる → ツールチップで表示
const x = [1, 2, 3].map((n) => n * 2);
//    ^? hover で number[] が表示される

// (2) Twoslash 風コメント:VSCode 拡張 "TypeScript Twoslash" で有効化
const y = { id: 1, name: "alice" };
//    ^?
// 上の ^? の行が、エディタ上でリアルタイムに型を表示してくれる

// (3) 型を変数に取り出して確認
type Inferred = typeof y;
//   ^? { id: number; name: string }

tsc / tsserver で型を吐く

// tsc --noEmit でビルドせず型エラーだけ確認
// $ npx tsc --noEmit

// .d.ts として宣言ファイルだけ生成して中身を読む
// $ npx tsc --declaration --emitDeclarationOnly --outDir types

// 推論結果を露出させる定型パターン
function debug<T>(x: T): T { return x; }
const v = debug({ a: 1, b: "x" });
//    ^? { a: number; b: string }

// expectType ヘルパ(@arktype/attest 等)
import { attest } from "@arktype/attest";
attest<typeof v>().equals<{ a: number; b: string }>();

パフォーマンス影響:推論コストと型コンプレキシティ

推論は無料ではありません。巨大な union や深い conditional type は、tsc のコンパイル時間を秒単位で押し上げることがあります。実プロジェクトでの対策を押さえます。

推論を重くするパターン

// ❌ 100要素超の as const は推論コスト増
const HUGE = [/* 1000要素のリテラル */] as const;
type HugeUnion = typeof HUGE[number];

// ❌ 深いネストの conditional type
type DeepMap<T> = T extends object
  ? { [K in keyof T]: DeepMap<T[K]> }
  : T extends string ? `prefix_${T}` : T;

// ✅ 対策1: 型エイリアスでキャッシュ
type CachedHuge = HugeUnion;

// ✅ 対策2: 大きな型は import type で型のみ取り込む
import type { GiantType } from "./giant";

// ✅ 対策3: 推論を諦めて type 注釈
const items: Item[] = [...]; // satisfies は推論し続けるので注釈の方が軽い

計測ツール

// tsc 自体に計測フラグがある
// $ npx tsc --extendedDiagnostics
// → Files / Lines / Identifiers / Memory / Total time が出力

// 詳細なトレース
// $ npx tsc --generateTrace ./.trace
// → Chrome DevTools の Performance タブで読み込める

// 個別ファイルのチェック時間
// $ npx tsc --diagnostics

// 型のインスタンス化深度を制限
// tsconfig.json
// {
//   "compilerOptions": {
//     "noImplicitAny": true,
//     "strict": true
//   }
// }

Before / After:推論を活かしたリファクタリング集

ここまでの内容を、実プロジェクトで頻出する「型を二重管理してしまっている」コードのリファクタリング例で総ざらいします。

例1:設定オブジェクトの二重定義

// ❌ Before:型と値を二重管理
type Routes = {
  home: string;
  about: string;
  contact: string;
};

const routes: Routes = {
  home: "/",
  about: "/about",
  contact: "/contact",
};

type RouteKey = keyof Routes; // "home" | "about" | "contact"

// ✅ After:値から型を導出
const routes = {
  home: "/",
  about: "/about",
  contact: "/contact",
} as const;

type RouteKey = keyof typeof routes;          // "home" | "about" | "contact"
type RoutePath = typeof routes[RouteKey];     // "/" | "/about" | "/contact"

例2:API レスポンスの型

// ❌ Before:戻り値型と実装の二重宣言
interface UserResponse {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<UserResponse> {
  const res = await fetch(`/users/${id}`);
  return res.json();
}

// ✅ After:zod 等のスキーマから推論
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(id: number) {
  const res = await fetch(`/users/${id}`);
  return UserSchema.parse(await res.json()); // 戻り値型は User と推論
}

例3:Reducer の型

// ❌ Before:Action 型を手書きで全部列挙
type Action =
  | { type: "INCREMENT" }
  | { type: "DECREMENT" }
  | { type: "RESET"; payload: number };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case "INCREMENT": return state + 1;
    case "DECREMENT": return state - 1;
    case "RESET":     return action.payload;
  }
}

// ✅ After:Action を実装から推論
const actions = {
  increment: () => ({ type: "INCREMENT" as const }),
  decrement: () => ({ type: "DECREMENT" as const }),
  reset: (n: number) => ({ type: "RESET" as const, payload: n }),
};

type Action = ReturnType<typeof actions[keyof typeof actions]>;
// 自動的に上と同じ union が生成される

例4:状態マシンの型安全化

// ❌ Before:状態名が string で型安全でない
const machine = {
  initial: "idle",
  states: { idle: {}, loading: {}, success: {}, error: {} },
};

function go(state: string) {
  return machine.states[state]; // any
}

// ✅ After:satisfies + as const で型安全に
const machine = {
  initial: "idle",
  states: {
    idle:    { on: { FETCH: "loading" } },
    loading: { on: { SUCCESS: "success", ERROR: "error" } },
    success: {},
    error:   { on: { RETRY: "loading" } },
  },
} as const satisfies {
  initial: string;
  states: Record<string, { on?: Record<string, string> }>;
};

type State = keyof typeof machine.states;  // "idle" | "loading" | ...
function go(s: State) {
  return machine.states[s]; // 具体的構造が見える
}

よくある推論失敗とトラブルシュート

Q1: any にされてしまう

// 原因例: callback の引数が contextual typing 文脈外
const handler = (e) => e.clientX; // e: any

// 対策: 関数型注釈
type Handler = (e: MouseEvent) => number;
const handler: Handler = (e) => e.clientX;

// または直接引数注釈
const handler = (e: MouseEvent) => e.clientX;

// tsconfig で strict / noImplicitAny を有効化して気づきやすくする

Q2: union に意図せず潰される

// 原因例: 戻り値が条件分岐ごとに違う型
function getItem(kind: "user" | "post") {
  if (kind === "user") return { id: 1, name: "alice" };
  return { id: 1, title: "post" };
}
type R = ReturnType<typeof getItem>;
// { id: number; name: string } | { id: number; title: string }

// 必要に応じて discriminator を入れる
function getItemTyped(kind: "user" | "post") {
  if (kind === "user") return { kind: "user" as const, id: 1, name: "alice" };
  return { kind: "post" as const, id: 1, title: "post" };
}

Q3: 推論結果が想定より広い

// 原因例: const なのに widening される(プリミティブ以外)
const config = { mode: "dev", port: 3000 };
// config.mode: string ← "dev" にならない

// 対策パターン
const config1 = { mode: "dev", port: 3000 } as const;        // 全部 readonly
const config2 = { mode: "dev" as const, port: 3000 };        // 個別に固定
const config3 = { mode: "dev", port: 3000 } satisfies Config;

Q4: filter で型が絞れない

// 原因: 通常の filter は型述語を使わないと型を絞らない

const xs: (string | undefined)[] = ["a", undefined, "b"];

// ❌
const ys = xs.filter((x) => x !== undefined); // (string | undefined)[]

// ✅ 型述語
const zs = xs.filter((x): x is string => x !== undefined); // string[]

// ✅ 汎用 NonNullable ガード
function isDefined<T>(x: T): x is NonNullable<T> { return x != null; }
const ws = xs.filter(isDefined); // string[]

Q5: ジェネリクスの型引数が unknown になる

// 原因: 引数から T が現れない関数
function create<T>(): T { return {} as T; }
const x = create(); // T 不明 → unknown

// 対策1: 呼び出し側で明示
const u = create<User>();

// 対策2: ファクトリ引数を入れて推論可能に
function createFromShape<T>(shape: T): T { return shape; }
const v = createFromShape({ id: 0, name: "" }); // T 推論成功

// 対策3: builder パターンで段階的に固める
class Builder<T = {}> {
  data: T;
  constructor(data: T) { this.data = data; }
  set<K extends string, V>(key: K, value: V) {
    return new Builder({ ...this.data, [key]: value } as T & Record<K, V>);
  }
}
const b = new Builder({}).set("id", 1).set("name", "alice");
// b.data: { id: number; name: string }

キャリアアップ:TypeScript の型推論を武器にする

ここまで読み切ったあなたは、すでにチーム内で「型に詳しい人」のポジションです。型推論を使いこなせるエンジニアは、ライブラリ実装・型安全なAPIクライアント設計・大規模リファクタリングで重宝されます。年収レンジで言えば、フロントエンドの実務経験+TypeScript深度がある人材は700〜1,000万円帯がボリュームゾーンになっています。

体系学習で抜け漏れを潰す

独学だと「inferを雰囲気で使っている」「satisfiesの使い分けが感覚」のままになりがちです。テックアカデミーのフロントエンドコースは現役エンジニアによるマンツーマンメンタリングがあり、ESLint設定・型設計・PR レビュー観点を含めた業務直結のフィードバックが受けられます。型推論のような「自分のコードを見せて初めて指摘される」領域とは特に相性が良いです。

転職で「TypeScript型に強い」を武器にする

React + TypeScript の求人は売り手市場が継続しており、特にライブラリ実装経験・OSSコントリビュート・型パズル(type challenges)を解いた経験のあるエンジニアは選考通過率が高い傾向です。侍エンジニアでは転職支援込みで現役エンジニア講師がマンツーマン対応してくれるため、ポートフォリオのコードレビュー段階から型設計を磨けます。

未経験から本気でモダンフロントを習得するなら

「そもそもReact/TypeScriptを業務レベルで身につけたい」段階の方には、DMM WEBCAMPのようなオンライン完結のコースが選択肢です。型推論を理解するためには JavaScript / モジュールシステム / 非同期処理の基礎が前提になるため、土台から固める方が結局速いです。

すでに現役なら、より高単価の現場へ

SES・自社開発・スタートアップなど現役エンジニアの方は、フリーランス案件で単価帯を引き上げるのも合理的です。レバテックフリーランスは React + TypeScript の案件保有数が多く、月単価80〜120万円帯の案件を担当営業経由で紹介してもらえます。「型推論まで解像度がある」エンジニアはコードレビュー要員としても期待されるため、面談で本記事のsatisfiesNoInferの使い分けを語れるだけで差別化になります。

まとめ:推論を味方につけるための7原則

  • 推論できる場所では型注釈を書かない:値と型の二重管理を避ける
  • const と let の違いを意識する:リテラル型保持か widening かが分かれる
  • as const と satisfies を使い分ける:readonly が欲しいか、制約だけ欲しいか
  • contextual typing を活かす:コールバックは関数型を期待する文脈で書く
  • narrowing は分岐の外で消える:必要ならローカル束縛で保持
  • filter には型述語を付ける:NonNullable で型を絞る習慣化
  • 困ったら hover と typeof で確認:推論結果の可視化を怠らない

型推論の解像度は、TypeScriptを書く速度と安全性の両方を引き上げる「投資対効果の最も高いスキル」です。一通り読み終わったら、ぜひ手元の業務コードでtypeofsatisfiesを試し、推論結果を hover で確認しながら、型と実装の二重管理を1つずつ潰してみてください。

関連記事:TypeScript型の基礎完全ガイド / TypeScriptジェネリクス完全ガイド / TypeScript Utility Types完全リファレンス / TypeScript完全実践ガイド

コメント

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