型注釈と型推論
明示的な型注釈と TypeScript の型推論の使い分けを学びます
このチャプターで学ぶこと
- TypeScript の型推論の仕組みを理解できる
- 型注釈が必要な場面と不要な場面を区別できる
- 関数のパラメータと戻り値の型注釈を書ける
- const と let での型推論の違いを理解できる
型注釈と型推論
前のチャプターでは変数や関数に型注釈(: string など)を付ける方法を学びました。しかし実際の TypeScript コードでは、すべてに型注釈を付ける必要はありません。TypeScript には 型推論(Type Inference) という強力な機能があり、多くの場面で型を自動的に判断してくれます。このチャプターでは「いつ型を書くべきか」「いつ省略してよいか」を学びます。
型推論とは
型推論とは、TypeScript コンパイラが代入された値や文脈から自動的に型を推測する機能です。
// 型注釈を書いた場合
const message: string = "こんにちは";
// 型推論に任せた場合(型注釈なし)
const message = "こんにちは"; // TypeScript が string 型だと自動判断する
VS Code で変数にマウスカーソルを合わせると、推論された型が表示されます。型注釈を省略しても、TypeScript は内部的に型を把握しています。
型推論は TypeScript の最も便利な機能のひとつです。冗長な型注釈を減らしてコードを読みやすく保ちつつ、型安全性はしっかり維持してくれます。PHP の PHPStan も同様の推論を行いますが、TypeScript はそれを言語レベルでサポートしています。
型推論が効く場面
変数の初期化
値を代入して初期化する変数は、型推論が効きます。
// すべて型推論が効く — 型注釈は不要
const name = "山田太郎"; // string と推論される
const age = 28; // number と推論される
const isActive = true; // boolean と推論される
const fruits = ["りんご", "バナナ"]; // string[] と推論される
const scores = [85, 92, 78]; // number[] と推論される
これらに : string や : number を書いても動作は同じですが、冗長になります。
関数の戻り値
関数の戻り値も、return 文の式から推論されます。
// 戻り値の型を書かなくても string と推論される
function greet(name: string) {
return `こんにちは、${name}さん!`;
}
// 複数の return がある場合もそれぞれを推論して統合する
function getLabel(count: number) {
if (count === 0) {
return "なし"; // string
}
return `${count}件`; // string
// 戻り値は string と推論される
}
配列メソッドのコールバック
map、filter、reduce などのコールバック関数の引数も推論されます。
const numbers = [1, 2, 3, 4, 5];
// num は number と推論される(numbers が number[] だから)
const doubled = numbers.map((num) => num * 2);
// item は string と推論される
const fruits = ["りんご", "バナナ", "みかん"];
const upperFruits = fruits.map((item) => item.toUpperCase());
// user の型はオブジェクトの型から推論される
const users = [
{ name: "太郎", age: 28 },
{ name: "花子", age: 25 },
];
const names = users.map((user) => user.name); // string[] と推論される
配列メソッドのコールバック引数に型注釈を書いている TypeScript 初心者のコードをよく見かけます。numbers.map((num: number) => ...) のように書いても間違いではありませんが、冗長です。TypeScript が自動で推論してくれるので省略しましょう。
const と let での型推論の違い
const と let では推論される型が異なります。これは TypeScript の重要な特徴です。
const — リテラル型として推論される
const status = "active";
// 型: "active"(string ではなく、"active" というリテラル型)
const count = 42;
// 型: 42(number ではなく、42 というリテラル型)
const isReady = true;
// 型: true(boolean ではなく、true というリテラル型)
const で宣言した変数は再代入できないため、TypeScript は「この変数は絶対にこの値しか取らない」と判断し、より狭い リテラル型 として推論します。
let — 一般的な型として推論される
let status = "active";
// 型: string("active" ではなく、string 全般)
let count = 42;
// 型: number(42 ではなく、number 全般)
let isReady = true;
// 型: boolean(true ではなく、boolean 全般)
let は後から別の値に再代入される可能性があるため、TypeScript はより広い型として推論します。
この違いが重要になる場面
// リテラル型を引数に要求する関数
function setTheme(theme: "light" | "dark") {
document.body.className = theme;
}
// const ならリテラル型 "light" なので OK
const theme1 = "light";
setTheme(theme1); // OK
// let だと string 型になるのでエラー
let theme2 = "light";
setTheme(theme2); // エラー: string は "light" | "dark" に代入できない
let で宣言した変数をリテラル型が必要な場面で使うと型エラーになります。解決策は以下のいずれかです:
constを使う(推奨)let theme: "light" | "dark" = "light"のように型注釈を書くsetTheme(theme2 as "light")のように型アサーションを使う(非推奨)
as const — オブジェクトや配列をリテラル型にする
const でオブジェクトを宣言しても、プロパティの値は string や number として推論されます。プロパティは再代入可能だからです。
// const で宣言しても、プロパティは広い型で推論される
const config = {
theme: "dark", // string と推論される("dark" ではない)
fontSize: 16, // number と推論される(16 ではない)
};
// as const を付けると、すべてリテラル型 + readonly になる
const config2 = {
theme: "dark", // "dark" と推論される
fontSize: 16, // 16 と推論される
} as const;
// readonly なので変更もできない
config2.theme = "light"; // エラー: readonly プロパティは変更できない
as const は設定オブジェクトや固定値の定義によく使われます。
型注釈が必要な場面
型推論は便利ですが、必ず型注釈を書くべき場面があります。
1. 関数のパラメータ
関数の引数は呼び出し元が決めるため、TypeScript は推論できません。必ず型注釈を書きましょう。
// パラメータに型注釈がないとエラー(strict: true の場合)
function greet(name) { // エラー: 暗黙の any
return `こんにちは、${name}さん!`;
}
// 正しい書き方
function greet(name: string) { // OK
return `こんにちは、${name}さん!`;
}
strict: true が有効な場合、パラメータの型注釈を省略すると Parameter 'name' implicitly has an 'any' type というエラーが出ます。これは noImplicitAny ルールによるもので、「型が分からないから any として扱うよ」という暗黙の動作を禁止しています。
2. 初期化なしの変数宣言
後から値を代入する変数は、宣言時に型を書く必要があります。
// 初期化なしでは型推論が効かない
let userName; // any と推論される(strict: true ならエラー)
userName = "山田太郎";
// 型注釈を書く
let userName: string; // OK
userName = "山田太郎";
3. 空の配列
空の配列は要素の型を推論できません。
// 空の配列は any[] になる
const items = []; // any[] と推論される
// 型注釈を書く
const items: string[] = []; // string[] として扱われる
items.push("りんご"); // OK
items.push(123); // エラー: number は string に代入できない
4. 関数の戻り値が複雑な場合
戻り値の型が複雑な場合や、意図を明示したい場合は型注釈を書くとコードが読みやすくなります。
// 推論に任せると戻り値の型が分かりにくい
function parseUserInput(input: string) {
if (input === "") {
return null;
}
const trimmed = input.trim();
if (trimmed.startsWith("@")) {
return { type: "mention" as const, value: trimmed.slice(1) };
}
return { type: "text" as const, value: trimmed };
}
// 推論される型: { type: "mention"; value: string } | { type: "text"; value: string } | null
// 明示した方が読みやすい
type ParseResult =
| { type: "mention"; value: string }
| { type: "text"; value: string }
| null;
function parseUserInput(input: string): ParseResult {
// ... 同じ実装
}
5. APIレスポンスなど外部データ
外部から受け取るデータは TypeScript が型を知りようがないので、型注釈が必須です。
// API レスポンスの型を定義
interface ApiResponse {
users: {
id: number;
name: string;
email: string;
}[];
total: number;
}
// fetch の結果に型を付ける
async function fetchUsers(): Promise<ApiResponse> {
const response = await fetch("/api/users");
const data: ApiResponse = await response.json();
return data;
}
Laravel の API リソース(JsonResource)で JSON の形を定義するのと同じように、TypeScript では interface や type で API レスポンスの形を定義します。フロントエンドとバックエンドで型を共有する仕組みも存在します(OpenAPI / tRPC など)。
関数の型注釈パターン
関数の書き方ごとに型注釈のパターンを整理しましょう。
関数宣言
// パラメータと戻り値に型注釈
function add(a: number, b: number): number {
return a + b;
}
// 戻り値は推論に任せてもOK
function add(a: number, b: number) {
return a + b; // number と推論される
}
アロー関数
// 型注釈付き
const add = (a: number, b: number): number => {
return a + b;
};
// 戻り値は推論に任せる
const add = (a: number, b: number) => a + b;
デフォルト引数がある場合
// デフォルト値から型が推論されるので型注釈は不要
function greet(name: string, greeting = "こんにちは") {
// greeting は string と推論される
return `${greeting}、${name}さん!`;
}
greet("太郎"); // "こんにちは、太郎さん!"
greet("太郎", "おはよう"); // "おはよう、太郎さん!"
greet("太郎", 123); // エラー: number は string に代入できない
オプショナルパラメータ
// ? を付けるとそのパラメータは省略可能になる
function createUser(name: string, age?: number) {
// age は number | undefined として扱われる
if (age !== undefined) {
return `${name}(${age}歳)`;
}
return name;
}
createUser("太郎", 28); // "太郎(28歳)"
createUser("太郎"); // "太郎"
オプショナルパラメータ(age?: number)は age: number | undefined と似ていますが、呼び出し時に引数自体を省略できるかどうかが異なります。? を付けた場合のみ引数を省略できます。
Rest パラメータ
// 可変長引数
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20, 30, 40); // 100
React コンポーネントでの型注釈
React コンポーネントでは props の型注釈が特に重要です。ここでは簡単な例を示します(詳しくは後のチャプターで扱います)。
// props の型をインラインで定義
function Greeting({ name, age }: { name: string; age: number }) {
return (
<div>
<h1>こんにちは、{name}さん!</h1>
<p>年齢: {age}歳</p>
</div>
);
}
// 使用例
function App() {
return <Greeting name="山田太郎" age={28} />;
}
props の型注釈があることで、コンポーネントを使うときに必要なプロパティとその型が VS Code の補完で表示されます。
// これらはすべてコンパイルエラーになる
<Greeting /> // name と age が必要
<Greeting name="太郎" /> // age が必要
<Greeting name="太郎" age="28" /> // age は number でなければならない
<Greeting name="太郎" age={28} extra="x" /> // extra は定義されていない
ベストプラクティス:関数境界で注釈、中は推論に任せる
TypeScript コミュニティで広く支持されているベストプラクティスは、「関数の境界(パラメータと公開APIの戻り値)には型注釈を書き、関数内部のローカル変数は推論に任せる」 というものです。
// 良い例:関数境界に型注釈、内部は推論に任せる
function processOrder(
items: CartItem[], // パラメータ — 型注釈を書く
discount: number // パラメータ — 型注釈を書く
): OrderSummary { // 戻り値 — 公開APIなので型注釈を書く
// ローカル変数は推論に任せる
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const discountAmount = subtotal * discount;
const total = subtotal - discountAmount;
const taxAmount = total * 0.1;
return {
subtotal,
discountAmount,
total,
taxAmount,
itemCount: items.length,
};
}
// 悪い例:すべてに型注釈を書いている(冗長)
function processOrder(
items: CartItem[],
discount: number
): OrderSummary {
const subtotal: number = items.reduce(
(sum: number, item: CartItem) => sum + item.price, 0
);
const discountAmount: number = subtotal * discount;
const total: number = subtotal - discountAmount;
const taxAmount: number = total * 0.1;
return {
subtotal,
discountAmount,
total,
taxAmount,
itemCount: items.length,
};
}
両者の動作は同じですが、上の方がはるかに読みやすく、メンテナンスしやすいコードです。
このルールを一言でまとめると「入口と出口だけ型を書く」です。関数のパラメータ(入口)と戻り値(出口)に型注釈を書き、内部のローカル変数は TypeScript の推論力に任せましょう。Laravel のコントローラーメソッドで引数の型ヒントと戻り値の型を書きつつ、メソッド内のローカル変数には型を書かないのと同じ感覚です。
型推論が期待と違うときの対処法
まれに TypeScript の推論が意図と異なる場合があります。
ケース 1: より狭い型が欲しい
// TypeScript は string と推論するが、特定の値だけにしたい
let status = "loading"; // string と推論される
// 解決策: 型注釈を書く
let status: "loading" | "success" | "error" = "loading";
ケース 2: ユニオン型にしたい
// number と推論されるが、null も許容したい
let result = 42; // number と推論される
// 解決策: 型注釈を書く
let result: number | null = 42;
result = null; // これがOKになる
ケース 3: 配列の型が広すぎる
// (string | number)[] と推論されるが、タプルにしたい
const pair = ["太郎", 28]; // (string | number)[] と推論される
// 解決策 1: 型注釈を書く
const pair: [string, number] = ["太郎", 28];
// 解決策 2: as const を使う
const pair = ["太郎", 28] as const; // readonly ["太郎", 28] と推論される
以下のコードを見て、各行の型注釈が 必要(推論できないので書くべき)か 冗長(推論に任せて省略してよい)かを判断してください。
// 問題: 各行の型注釈は必要?冗長?
// (1)
const title: string = "TypeScriptの基本";
// (2)
let count: number = 0;
// (3)
function multiply(a: number, b: number): number {
return a * b;
}
// (4)
const users: { name: string; age: number }[] = [];
// (5)
const result: number = [1, 2, 3].reduce((sum, n) => sum + n, 0);
// (6)
function fetchData(url: string): Promise<unknown> {
return fetch(url).then((res) => res.json());
}
// (7)
let selectedId: number | null = null;
// (8)
const numbers: number[] = [1, 2, 3, 4, 5];
// (9)
const doubled: number[] = numbers.map((n: number) => n * 2);
// (10)
function greet(name: string, greeting: string = "こんにちは") {
return `${greeting}、${name}さん!`;
}解答:
// (1) 冗長 — "TypeScriptの基本" から string と推論される
const title = "TypeScriptの基本";
// (2) 場合による — 後で null を代入するなら必要、しないなら冗長
let count = 0; // number と推論される
// (3) 戻り値は冗長 — return 文から number と推論される。パラメータは必要
function multiply(a: number, b: number) {
return a * b;
}
// (4) 必要 — 空の配列からは要素の型が推論できない
const users: { name: string; age: number }[] = [];
// (5) 冗長 — reduce の結果が number と推論される
const result = [1, 2, 3].reduce((sum, n) => sum + n, 0);
// (6) 必要 — 外部データの型は明示すべき(意図の表現として有用)
function fetchData(url: string): Promise<unknown> {
return fetch(url).then((res) => res.json());
}
// (7) 必要 — null と number の両方を許容することを明示
let selectedId: number | null = null;
// (8) 冗長 — [1, 2, 3, 4, 5] から number[] と推論される
const numbers = [1, 2, 3, 4, 5];
// (9) 冗長(二重に冗長)— map のコールバック引数も、結果の型も推論される
const doubled = numbers.map((n) => n * 2);
// (10) greeting の型は冗長 — デフォルト値 "こんにちは" から string と推論される
// ただし name の型注釈は必要
function greet(name: string, greeting = "こんにちは") {
return `${greeting}、${name}さん!`;
}判断のポイントをまとめると:
const+ 初期値あり → ほぼ冗長(リテラル型が必要な場合は除く)- 関数のパラメータ → 必要(デフォルト値がある場合は推論可能)
- 空の配列 → 必要
nullを許容する変数 → 必要- コールバック関数の引数 → ほぼ冗長
- 外部データの型 → 必要(安全性のため)
まとめ
このチャプターでは型注釈と型推論の使い分けを学びました。
- 型推論 — TypeScript が値や文脈から自動的に型を判断する機能。多くの場面で型注釈を省略できる
constはリテラル型として推論され、letは一般的な型として推論される- 型注釈が必要な場面 — 関数パラメータ、初期化なしの変数、空の配列、外部データ、null 許容変数
- 型注釈が冗長な場面 —
const+ 初期値、関数のローカル変数、コールバック引数、推論可能な戻り値 - ベストプラクティス — 「関数の境界で注釈、中は推論に任せる」
この使い分けは最初のうちは迷うかもしれませんが、VS Code で変数にカーソルを合わせて推論結果を確認する習慣をつけると、徐々に感覚が身についてきます。次のチャプターでは interface と type を使って、再利用可能な型定義を作る方法を学びます。