TypeScript の強力な機能のひとつが 型推論(Type Inference) です。型を明示しなくても TypeScript が自動的に型を決定するため、冗長な型注釈を減らしつつ型安全を維持できます。
本記事では型推論の仕組みを基礎から解説し、infer キーワード・as const との組み合わせ・型推論が効かない場面など、実務で必要な知識をすべて実例付きで紹介します。
- TypeScript が型を自動判定する仕組み
- let / const・オブジェクト・配列での型推論の違い
- 関数戻り値・コンテキスト型推論のパターン
- ジェネリクス呼び出しでの型引数自動推論
inferで条件型から型を抽出する方法as constでリテラル型に固定する方法- 明示的型付けが必要な場面と判断基準
1. 型推論とは
型推論とは、変数や関数の型を明示的に書かなくても TypeScript が文脈から自動的に型を決定する機能です。
| 推論の種類 | 説明 | 例 |
|---|---|---|
| 変数初期化推論 | 代入値から変数の型を決定 | let x = 42 → number |
| 戻り値推論 | return 文から関数の戻り値型を決定 | function f() { return "hi" } → string |
| コンテキスト型推論 | 使われる文脈から型を決定(右辺から左辺へ) | イベントハンドラの引数など |
| 型引数推論 | 関数呼び出し時の引数からジェネリクス型を決定 | identity(42) → T = number |
| 制御フロー推論 | 条件分岐後に型を絞り込む | if (typeof x === "string") 後は string |
型推論が効く場面では型注釈を省略できますが、関数の引数型・パブリック API の戻り値型・複雑な型が推論されるケースでは明示的に書くとコードの意図が明確になります。「推論できるから書かない」のではなく、読む人への伝達を意識して判断しましょう。
2. 変数宣言での型推論
2-1. let と const の違い
let と const では推論される型が異なります。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) => ...) は n が number と推論される文脈で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 キーワードは条件型の中で使い、型のある部分を変数として「キャプチャ」できます。ユーティリティ型 ReturnType や Parameters の実装にも使われています。
// 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: true(strict モードに含まれる)を有効にすると、型推論できずに暗黙的に any になる箇所でエラーが発生します。主に関数引数(推論されないため)や型注釈なしの変数宣言(文脈から型が決まらない場合)が対象です。有効化することで型安全性が向上するため、新規プロジェクトでは推奨されます。
Q型推論された型を確認するにはどうすればよいですか?
AVSCode などのエディタで変数にカーソルを当てるとツールチップに推論された型が表示されます。また type Debug = typeof 変数名 と書いてエディタで確認する方法もあります。CI 環境では tsc --noEmit で型チェックのみ実行して推論エラーを確認できます。
QReturnType と型注釈はどちらを使うべきですか?
A関数が実装の詳細であり戻り値型は内部から決まる場合は ReturnType が便利です(変更時に自動追従)。一方、関数が公開インターフェースとして明確な型を持つべき場合は型注釈で明示します。特に async 関数では Promise<User> のように明示すると呼び出し元が型情報を得やすくなります。

