【TypeScript】型推論(Type Inference)完全ガイド|自動推論の仕組み・infer・as const・実践パターンまで徹底解説

【TypeScript】型推論(Type Inference)完全ガイド|自動推論の仕組み・infer・as const・実践パターンまで徹底解説 TypeScript

TypeScript の強力な機能のひとつが 型推論(Type Inference) です。型を明示しなくても TypeScript が自動的に型を決定するため、冗長な型注釈を減らしつつ型安全を維持できます。

本記事では型推論の仕組みを基礎から解説し、infer キーワード・as const との組み合わせ・型推論が効かない場面など、実務で必要な知識をすべて実例付きで紹介します。

この記事でわかること

  • TypeScript が型を自動判定する仕組み
  • let / const・オブジェクト・配列での型推論の違い
  • 関数戻り値・コンテキスト型推論のパターン
  • ジェネリクス呼び出しでの型引数自動推論
  • infer で条件型から型を抽出する方法
  • as const でリテラル型に固定する方法
  • 明示的型付けが必要な場面と判断基準
スポンサーリンク

1. 型推論とは

型推論とは、変数や関数の型を明示的に書かなくても TypeScript が文脈から自動的に型を決定する機能です。

推論の種類 説明
変数初期化推論 代入値から変数の型を決定 let x = 42number
戻り値推論 return 文から関数の戻り値型を決定 function f() { return "hi" }string
コンテキスト型推論 使われる文脈から型を決定(右辺から左辺へ) イベントハンドラの引数など
型引数推論 関数呼び出し時の引数からジェネリクス型を決定 identity(42)T = number
制御フロー推論 条件分岐後に型を絞り込む if (typeof x === "string") 後は string
型推論と明示的型付けの使い分け
型推論が効く場面では型注釈を省略できますが、関数の引数型・パブリック API の戻り値型・複雑な型が推論されるケースでは明示的に書くとコードの意図が明確になります。「推論できるから書かない」のではなく、読む人への伝達を意識して判断しましょう。

2. 変数宣言での型推論

2-1. let と const の違い

letconst では推論される型が異なります。const は再代入不可なためリテラル型に、let はより広い型に推論されます。

// const → リテラル型に推論される
const name = "Alice";    // 型: "Alice"(string ではなく文字列リテラル型)
const count = 42;        // 型: 42(number ではなく数値リテラル型)
const flag = true;       // 型: true(boolean ではなく)

// let → より広い型に推論される
let name2 = "Alice";     // 型: string
let count2 = 42;         // 型: number
let flag2 = true;        // 型: boolean

// 型注釈なしで代入できる範囲が変わる
name2 = "Bob";           // OK(string なので)
// name = "Bob";         // Error("Alice" 型なので他の値は代入不可)

2-2. オブジェクトの型推論

// オブジェクトリテラルはプロパティの型から推論
const user = {
    name: "Alice",   // string
    age: 30,         // number
    active: true,    // boolean
};
// user の型: { name: string; age: number; active: boolean }

// プロパティは let と同様に広い型で推論される
user.name = "Bob";   // OK
// user.name = 123;  // Error: number は string に代入不可

// as const でプロパティをリテラル型に固定
const config = {
    env: "production",
    port: 3000,
} as const;
// config.env の型: "production"(string ではない)
// config.port の型: 3000(number ではない)
// config.env = "development"; // Error: readonly

2-3. 配列・タプルの型推論

// 配列リテラルは要素の Union 型で推論
const nums = [1, 2, 3];           // 型: number[]
const mixed = [1, "hello", true]; // 型: (number | string | boolean)[]

// as const でタプル型として推論
const pair = [1, "Alice"] as const;
// 型: readonly [1, "Alice"](タプル型・各要素がリテラル型)

// 空配列は never[] に推論されるため明示が必要
const items = [];           // 型: never[](要素を追加できない)
const items2: string[] = []; // OK: 明示的に型を指定

3. 関数での型推論

3-1. 戻り値の自動推論

関数の戻り値型は return 文から自動推論されます。複数の return がある場合は Union 型になります。

// 戻り値型が自動推論される
function greet(name: string) {
    return `Hello, ${name}!`;  // 戻り値型: string
}

// 複数 return → Union 型
function divide(a: number, b: number) {
    if (b === 0) return null;  // null
    return a / b;             // number
}  // 戻り値型: number | null

// 明示した方がよいケース:公開 API や意図の明確化
function createUser(name: string): { id: number; name: string } {
    return { id: Math.random(), name };
    // 戻り値型を明示することで「契約」として機能する
}

3-2. コンテキスト型推論(Contextual Typing)

コンテキスト型推論は「右辺から左辺への推論」ではなく、使われる場所の型から引数の型が決まるパターンです。コールバック関数でよく現れます。

// addEventListener のコールバック: event の型が自動で MouseEvent に推論される
document.addEventListener("click", (event) => {
    console.log(event.clientX); // OK: event は MouseEvent 型
});

// Array.prototype.map のコールバック: 引数型が推論される
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
// n の型: number(numbers が number[] なので)
// doubled の型: number[]

// オブジェクトの型注釈からプロパティの型が推論される
const handler: { onClick: (e: MouseEvent) => void } = {
    onClick: (e) => {         // e の型: MouseEvent(コンテキストから推論)
        console.log(e.target);
    },
};
コールバックの引数に型注釈を付けすぎない
コンテキスト型推論が効く場面で引数に明示的な型を付けると、型が合わない場合にエラーになることがあります。例えば numbers.map((n: string) => ...)nnumber と推論される文脈でstring を指定しているためエラーになります。コールバック引数は原則として型推論に任せましょう。

4. ジェネリクスの型推論

4-1. 関数呼び出し時の型引数自動推論

// ジェネリクス関数: T は呼び出し時の引数から推論される
function identity<T>(value: T): T {
    return value;
}

const a = identity(42);       // T = number に推論→ a: number
const b = identity("hello");  // T = string に推論→ b: string
const c = identity<string>(42); // Error: number は string に代入不可

// 複数引数から推論
function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}
const p = pair(1, "hello"); // [number, string]

// 配列から要素型を推論
function first<T>(arr: T[]): T | undefined {
    return arr[0];
}
const item = first([1, 2, 3]); // T = number → item: number | undefined

4-2. ジェネリクス型引数の制約と推論

// extends による制約があっても型引数は推論される
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // T = typeof user, K = "name"
                                         // 戻り値型: string
const age = getProperty(user, "age");   // 戻り値型: number
// getProperty(user, "email");          // Error: "email" は keyof user にない

// デフォルト型引数
function createState<T = string>(initial: T) {
    return { value: initial };
}
const s1 = createState("hello");  // T = string(引数から推論)
const s2 = createState<number>(); // Error: 引数が必要
const s3 = createState(42);       // T = number

ジェネリクスの詳細は ジェネリクス(Generics)完全ガイド を参照してください。

5. 高度な推論パターン

5-1. infer で条件型から型を抽出する

infer キーワードは条件型の中で使い、型のある部分を変数として「キャプチャ」できます。ユーティリティ型 ReturnTypeParameters の実装にも使われています。

// ReturnType の実装(標準ライブラリより)
// type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 使用例: 関数の戻り値型を取得
function fetchUser() {
    return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof fetchUser>;
// User = { id: number; name: string }

// Parameters: 関数の引数型をタプルで取得
function createPost(title: string, body: string, tags: string[]) {}
type PostArgs = Parameters<typeof createPost>;
// PostArgs = [title: string, body: string, tags: string[]]

// カスタム infer: Promise の解決型を取り出す
type Awaited<T> = T extends Promise<infer U> ? U : T;
type ResolvedType = Awaited<Promise<string>>; // string
type PlainType = Awaited<number>;             // number(Promise でないのでそのまま)

// ネストした Promise にも対応
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Deep = DeepAwaited<Promise<Promise<string>>>; // string

5-2. as const によるリテラル型推論

// as const で値全体をリテラル型に固定
const STATUS = {
    ACTIVE: "active",
    INACTIVE: "inactive",
    PENDING: "pending",
} as const;

// as const なし → 型: { ACTIVE: string; INACTIVE: string; PENDING: string }
// as const あり → 型: { readonly ACTIVE: "active"; readonly INACTIVE: "inactive"; readonly PENDING: "pending" }

// Union 型を自動生成できる
type Status = typeof STATUS[keyof typeof STATUS];
// Status = "active" | "inactive" | "pending"

// 配列を as const でタプルとして扱う
const ROUTES = ["/", "/about", "/contact"] as const;
type Route = typeof ROUTES[number]; // "/" | "/about" | "/contact"

// 関数引数での as const
function move(direction: "up" | "down" | "left" | "right") {}
const dir = "up" as const; // "up" 型(string ではない)
move(dir); // OK

as const の詳細は 型アサーション完全ガイド を参照してください。

5-3. 制御フロー分析による型の絞り込み

// TypeScript は if/switch 後の型を自動的に絞り込む
function process(value: string | number | null) {
    if (value === null) {
        // value: null
        return "null value";
    }
    // ここでは value: string | number

    if (typeof value === "string") {
        // value: string
        return value.toUpperCase();
    }
    // ここでは value: number
    return value.toFixed(2);
}

// 代入後も型が更新される
let x: string | number = "hello";
// x: string | number

x = 42;
// x: number(代入後は number に絞り込まれる)

console.log(x.toFixed(1)); // OK

6. 型推論が苦手な場面・明示的型付けが必要なケース

場面 問題 対策
関数の引数 引数型は推論されない(noImplicitAny でエラー) 引数型を明示: (x: string)
空配列の初期化 never[] に推論される const arr: string[] = []
複雑なオブジェクトの戻り値 推論が深すぎてエラーになることがある 戻り値型を明示
再帰的な型 推論が循環しエラーになる 型エイリアスで明示的に定義
型が広すぎる推論 string で推論されるが "active" | "inactive" が必要 as const か Union 型を明示
クラスのプロパティ(宣言のみ) 初期化なしは型不明 型注釈で明示: name: string
// 引数型は必ず明示する
function double(n) { return n * 2; } // Error: n implicitly has 'any' type
function double(n: number) { return n * 2; } // OK

// 空配列は明示する
const tags = [];           // never[]
const tags2: string[] = []; // OK

// 複雑な戻り値は明示で意図を示す
interface ApiResponse<T> { data: T; status: number; }

function fetchUsers(): Promise<ApiResponse<User[]>> {
    // 戻り値型を明示することで呼び出し元が型情報を得やすい
    return fetch("/api/users").then(r => r.json());
}
型推論を活かすベストプラクティス

  • 変数: 初期化時は型推論に任せる(const x = 42
  • 関数引数: 必ず明示する(推論されない)
  • 関数戻り値: 内部実装は推論に任せ、公開 API は明示する
  • 空配列・空オブジェクト: 使用前に型を明示する
  • as const: リテラル型が必要な定数定義に使う

7. 実践例3本

実践例1:ReturnType で関数の戻り値型を再利用する

関数の戻り値型を別の場所で使いたいとき、型を二重定義せず ReturnType で推論から取得するパターンです。

// ユーザー取得関数
async function getUser(id: number) {
    const res = await fetch(`/api/users/${id}`);
    const data = await res.json();
    return {
        id: data.id as number,
        name: data.name as string,
        email: data.email as string,
        createdAt: new Date(data.createdAt as string),
    };
}

// ReturnType で戻り値型を取得(interface を別途書かなくてよい)
type User = Awaited<ReturnType<typeof getUser>>;
// User = { id: number; name: string; email: string; createdAt: Date }

// コンポーネントや他の関数で再利用
function renderUser(user: User): string {
    return `${user.name} (${user.email})`;
}

// getUser の実装を変更すれば User 型も自動更新される(DRY 原則)

実践例2:as const で設定オブジェクトから型を自動生成する

設定値を as const で定義することで、Union 型やタプル型を手動で書かずに自動生成できます。

// as const で設定値を定義
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
type HttpMethod = typeof HTTP_METHODS[number];
// HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"

const THEME = {
    colors: {
        primary: "#0284c7",
        secondary: "#64748b",
        danger: "#ef4444",
    },
    spacing: [4, 8, 12, 16, 24, 32] as const,
} as const;

type ThemeColor = typeof THEME.colors[keyof typeof THEME.colors];
// ThemeColor = "#0284c7" | "#64748b" | "#ef4444"

type Spacing = typeof THEME.spacing[number];
// Spacing = 4 | 8 | 12 | 16 | 24 | 32

// 型が設定値と同期されるため、設定変更時に型も自動更新
function applySpacing(size: Spacing): string {
    return `${size}px`;
}
applySpacing(8);   // OK
// applySpacing(10); // Error: 10 は Spacing 型にない

実践例3:infer でミドルウェア型パターンを実装する

infer を使って、ラップされた型を自動展開するユーティリティ型を実装するパターンです。

// Express 風のミドルウェア型
type Middleware<T> = (req: T, next: () => void) => void;

// Middleware<T> から T を取り出す infer パターン
type MiddlewareRequest<M> = M extends Middleware<infer R> ? R : never;

type AuthMiddleware = Middleware<{ userId: string; token: string }>;
type AuthRequest = MiddlewareRequest<AuthMiddleware>;
// AuthRequest = { userId: string; token: string }

// 実践: 複数ミドルウェアの型を合成
type MergeRequests<T extends Middleware<any>[]> =
    T extends [Middleware<infer First>, ...infer Rest]
        ? Rest extends Middleware<any>[]
            ? First & MergeRequests<Rest>
            : First
        : {};

type Combined = MergeRequests<[
    Middleware<{ userId: string }>,
    Middleware<{ sessionId: string }>,
    Middleware<{ locale: string }>,
]>;
// Combined = { userId: string } & { sessionId: string } & { locale: string }

条件型・infer の詳細は 高度な型完全ガイド を参照してください。

8. まとめ:型推論の使い分け早見表

場面 推奨アプローチ 理由
変数の初期化 型推論に任せる 冗長な型注釈を避ける
関数の引数 必ず明示 引数は推論されない
関数の戻り値(内部) 型推論に任せる return 文から自動推論
関数の戻り値(公開 API) 明示推奨 意図・契約を明確にする
定数オブジェクト・配列 as constでリテラル型化 Union型を自動生成できる
空配列・空オブジェクト 型注釈で明示 never[]になるのを防ぐ
関数の戻り値型を再利用 ReturnTypeで取得 DRY・変更時に自動追従
型の一部を抽出したい inferで条件型を使う 柔軟な型ユーティリティを作れる

TypeScript の型システム全般については 型の書き方 完全入門 も参照してください。

FAQ

Q型推論に頼りすぎると可読性が下がりませんか?

A型推論が効く単純な変数宣言では注釈を省略してもコードは読みやすいです。一方、関数シグネチャ・公開API・型が複雑になる箇所では明示的な型注釈が「ドキュメント」として機能します。「推論できるから書かない」ではなく、読む人への伝達を意識して使い分けるのが良いプラクティスです。

Qconst と let で推論される型が違うのはなぜですか?

Aconst は再代入不可なため TypeScript は「この値が変わらない」と判断し、より具体的なリテラル型(例:"hello")を推論します。let は再代入可能なため、より広い型(例:string)に推論されます。リテラル型が必要な場面で let を使うときは as const を付けるか型注釈を明示してください。

Qinfer はどんな場面で使いますか?

Ainfer は条件型(T extends X ? A : B)の中で型の一部を「変数として取り出す」ときに使います。代表的な用途は ReturnType(関数の戻り値型)・Parameters(引数型のタプル)・Awaited(Promiseの解決型)の実装です。独自のユーティリティ型を作る際に非常に強力です。

QnoImplicitAny を有効にすると型推論はどう変わりますか?

AnoImplicitAny: truestrict モードに含まれる)を有効にすると、型推論できずに暗黙的に any になる箇所でエラーが発生します。主に関数引数(推論されないため)や型注釈なしの変数宣言(文脈から型が決まらない場合)が対象です。有効化することで型安全性が向上するため、新規プロジェクトでは推奨されます。

Q型推論された型を確認するにはどうすればよいですか?

AVSCode などのエディタで変数にカーソルを当てるとツールチップに推論された型が表示されます。また type Debug = typeof 変数名 と書いてエディタで確認する方法もあります。CI 環境では tsc --noEmit で型チェックのみ実行して推論エラーを確認できます。

QReturnType と型注釈はどちらを使うべきですか?

A関数が実装の詳細であり戻り値型は内部から決まる場合は ReturnType が便利です(変更時に自動追従)。一方、関数が公開インターフェースとして明確な型を持つべき場合は型注釈で明示します。特に async 関数では Promise<User> のように明示すると呼び出し元が型情報を得やすくなります。