TypeScriptの基礎 - 型推論
概要
TypeScriptの型推論とは、変数や関数の型を明示的に宣言しなくても、TypeScriptコンパイラが文脈から型を自動的に決定する機能である。
変数の初期値への代入、関数の戻り値、パラメータのデフォルト値など、多くの場面でコンパイラが型を正確に推定する。
例えば、let count = 0; と記述するだけで、コンパイラは count を number 型と認識する。
これにより、型注釈を省略しながらも型安全性を確保でき、ソースコードの簡潔さと可読性を両立できる。
型推論には大きく2つの方向がある。
1つは初期値や戻り値から型を決定する通常の推論であり、複数の候補がある場合は 最良共通型アルゴリズム によって最適な型が選択される。
もう1つはコンテキスト型推論であり、イベントハンドラのコールバック引数など、使用箇所の文脈から型が決まる。
一方で、型推論に頼りすぎることが適切でない場面もある。
関数のパラメータにデフォルト値が無い場合、空配列・空オブジェクトの初期化時、あるいは、外部APIやJSONの解析結果を扱う場合は、
コンパイラが any 型にフォールバックしてしまうため、明示的な型注釈を付与することが推奨される。
型推論はTypeScriptの中核をなす機能である。
その動作原理と適用範囲を正しく理解することで、冗長な型注釈を排除しながら、堅牢で保守性の高いソースコードを記述できる。
型推論の基本
TypeScriptは、変数の初期値や式の内容から型を自動的に推定する。
この仕組みにより、型注釈を省略しても型安全なコードを記述できる。
変数初期化時の推論
変数を宣言と同時に値で初期化すると、TypeScriptはその値の型を推論する。
let count = 5; // 型: number
let name = "Alice"; // 型: string
let isActive = true; // 型: boolean
推論される型は初期値のリテラルではなく、そのプリミティブ型になる。
例えば、let count = 5 は、値が 5 であっても型は number 型と推論され、後から別の数値を代入できる。
初期化を伴わない変数宣言では型を推論できないため、any 型とみなされる。
noImplicitAny を有効にしている場合はコンパイルエラーとなる。
let と constの推論結果の違い
let と const では、同じ値で初期化しても推論される型が異なる。
これは、変数が再代入可能かどうかによって、コンパイラが最適な型を選択するためである。
let x = "hello"; // 型: string (再代入可能なため、ワイドな型に推論)
const y = "hello"; // 型: "hello" (再代入不可なため、リテラル型に推論)
let num = 42; // 型: number
const pi = 3.14; // 型: 3.14
const で宣言された変数は値が変わらないことが保証されるため、TypeScriptはリテラル型 (例: "hello") を割り当てる。
一方、let で宣言された変数は再代入される可能性があるため、より汎用的なプリミティブ型 (例: string) へとワイドニングされる。
この挙動は 型ワイドニング (Type Widening) と呼ばれる。
配列の型推論
配列リテラルに対しても、TypeScriptは要素の型から配列の型を推論する。
const numbers = [1, 2, 3]; // 型: number[]
const words = ["foo", "bar"]; // 型: string[]
// 異なる型の要素が混在する場合はユニオン型になる
const mixed = [1, "hello"]; // 型: (string | number)[]
// strictNullChecks有効時
const withNull = [1, null]; // 型: (number | null)[]
複数の異なる型の要素が含まれる場合、TypeScriptは全ての要素を包括するユニオン型を推論する。
この選択アルゴリズムは最良共通型 (Best Common Type) と呼ばれる。
空配列 [] は型を推論できないため、never[] になってしまう場合がある。
空配列を初期化するときは型注釈を明示することが推奨される。
// 空配列には型注釈が必要
const items: string[] = [];
関数における型推論
TypeScriptは、関数の戻り値の型をreturn文から推論する。
一方、関数のパラメータは使用方法から自動推論されないため、原則として型注釈が必要である。
戻り値の推論
関数のreturn文の式からTypeScriptは戻り値の型を推論する。
// 戻り値はnumberと推論される
function add(a: number, b: number) {
return a + b;
}
// 戻り値はstringと推論される
function greet(name: string) {
return `Hello, ${name}`;
}
// 条件に応じた戻り値はユニオン型になる
function getIdOrName(useId: boolean) {
if (useId) {
return 42; // number
}
else {
return "Alice"; // string
}
}
// 戻り値の型: string | number
複数のreturn文が異なる型を返す場合、それら全てを包括するユニオン型が推論される。
再帰関数や複雑な制御フローでは、推論が意図通りにならない場合もある。
公開APIの関数では、戻り値の型を明示的に注釈しておくことが推奨される。
引数の型推論が行われないケース
関数のパラメータは、呼び出し元の引数から推論されない。
TypeScriptでは、パラメータには常に型注釈を明示する必要がある。
// パラメータに型注釈がない場合
function double(x) { // エラー: パラメータ 'x' に暗黙的に 'any' 型が含まれます
return x * 2;
}
// 正しい書き方 : 型注釈を明示する
function double(x: number) {
return x * 2;
}
noImplicitAny を有効にしている場合 (推奨設定)、型注釈のないパラメータはコンパイルエラーとなる。
パラメータは常に型を明示することにより、関数の意図を明確にして、型安全性を確保できる。
コンテキスト型推論 (Contextual Typing)
コールバック関数の引数は、その使用される文脈 (コンテキスト) から型が推論される。
これをコンテキスト型推論 (Contextual Typing) と呼ぶ。
const numbers = [1, 2, 3];
// itemの型はnumberと推論される
numbers.forEach((item) => {
console.log(item.toFixed(2)); // item が number なので toFixed() が利用可能
});
// nの型はnumberと推論される
const doubled = numbers.map((n) => n * 2);
// eventの型はMouseEventと推論される
document.addEventListener("click", (event) => {
console.log(event.clientX); // MouseEvent のプロパティが利用可能
});
コンテキスト型推論は、配列メソッド (forEach、map、filter 等) やイベントリスナー等、多くの場面で機能する。
この推論により、コールバック引数への型注釈が不要となり、処理が簡潔になる。
コンテキスト型推論が正しく機能するには、呼び出し先の関数が適切な型定義を持っている必要がある。
標準ライブラリや型定義ファイル (.d.ts拡張子) で型が定義されていない関数では推論されない。
型推論の詳細な挙動
TypeScriptの型推論には、最良共通型、型ワイドニング、型ナローイングといった詳細な概念がある。
これらを理解することにより、型推論の結果を予測し、意図した型を得られるようになる。
最良共通型 (Best Common Type)
複数の異なる型の値が存在する場合、TypeScriptはそれらを包括できる最適な共通型 (Best Common Type) を選択する。
// 数値とnullが混在する配列
const arr = [0, 1, null]; // 型: (number | null)[] (strictNullChecks有効時)
// 三項演算子
const value = Math.random() > 0.5 ? "hello" : 42; // 型: string | number
// クラスの共通基底型
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
const pets = [new Dog(), new Cat()]; // 型: (Dog | Cat)[]
最良共通型の選択は、候補となる型全てのサブタイプとなる型を優先する。
共通の基底クラスや共通インターフェースが存在する場合、それが選択される場合もある。
候補となる型が互いにサブタイプ関係にない場合は、それらのユニオン型が最良共通型となる。
型ワイドニング (Type Widening)
let で宣言された変数にリテラル値を代入すると、TypeScriptはより汎用的な型へと変換する。
これを 型ワイドニング (Type Widening) と呼ぶ。
// let : リテラル "hello"からstringへとワイドニングされる
let x = "hello"; // 型: string (リテラル型 "hello" ではない)
x = "world"; // OK: string 型なので別の文字列を代入できる
// const : 再代入不可なのでリテラル型のまま
const y = "hello"; // 型: "hello"
// オブジェクトのプロパティも同様にワイドニングされる
const obj = {
host: "localhost", // 型: string (リテラル "localhost" ではない)
port: 3000, // 型: number (リテラル 3000 ではない)
};
型ワイドニングは、変数が後から別の値に変更される可能性を考慮した安全な設計によるものである。
しかし、リテラル型として型を保持したい場面では、as const を使用して型ワイドニングを防止できる。
as constによる型ワイドニングの防止
as const アサーションを使用すると、型ワイドニングを防止してリテラル型を保持できる。
// let変数でもリテラル型を保持
let z = "hello" as const; // 型: "hello" (ワイドニングされない)
// オブジェクト全体にas constを適用
const config = {
host: "localhost",
port: 3000,
} as const;
// 型 : { readonly host: "localhost"; readonly port: 3000 }
// 全プロパティがreadonlyかつリテラル型になる
// 配列にas constを適用
const directions = ["north", "south", "east", "west"] as const;
// 型 : readonly ["north", "south", "east", "west"]
// 各要素がリテラル型になり、タプル型として固定される
as const を適用すると以下に示す効果がある。
- プロパティの型がリテラル型になる。
- オブジェクトや配列の全プロパティが
readonlyになる。 - 配列はタプル型として推論される。
as const は、列挙的な定数セットや設定オブジェクトを定義する時に有用である。
型の詳細については、TypeScriptの基礎 - 型アサーションのページを参照すること。
型ナローイング (Type Narrowing) の概要
型ナローイングとは、ユニオン型等の広い型を条件分岐によってより具体的な型へと絞り込む仕組みである。
TypeScriptはソースコードの制御フローを解析して、特定のブロック内での型を自動的に絞り込む。
function printValue(value: string | number) {
// ここではvalueの型は string | number
if (typeof value === "string") {
// ここではvalueの型がstringに絞り込まれる
console.log(value.toUpperCase()); // stringのメソッドが利用可能
}
else {
// ここではvalueの型がnumberに絞り込まれる
console.log(value.toFixed(2)); // numberのメソッドが利用可能
}
}
代表的なナローイングの手法を以下に示す。
| 手法 | 説明 |
|---|---|
typeof 演算子 |
typeof value === "string" でstring型に絞り込む。 |
instanceof 演算子 |
value instanceof Date でDate型に絞り込む。 |
in 演算子 |
"name" in value でプロパティの有無により型を絞り込む。 |
| 等価チェック | value === null でnullを除外する。 |
型ナローイングの詳細は、TypeScriptの基礎 - 型の絞り込み(Type Narrowing)のページを参照すること。
推論に頼ってよい場面と明示すべき場面
TypeScriptの型推論は強力だが、全ての場面で推論に頼るべきではない。
型推論と型注釈を適切に使い分けることにより、読みやすく保守性の高いソースコードを実現できる。
推論に頼ってよい場面
型が一目見て明らかな場合は、型推論に任せてコードを簡潔に保つのが適切である。
| 場面 | 説明 |
|---|---|
| 変数初期化時 (値から型が自明な場合) | let count = 0 や let name = "Alice" では型注釈は不要 |
| 関数の戻り値 (単純な場合) | function double(x: number) { return x * 2; } の戻り値 number型は明白であるため。 |
| コールバック関数の引数 | array.forEach((item) => ...) のコンテキスト型推論に任せる。 |
| ローカル変数の一時的な格納 | 関数内の一時変数は型推論で十分なことが多い。 |
// 推論に頼ってよい例
const users = ["Alice", "Bob", "Carol"]; // string[] と明らか
const total = users.length; // number と明らか
users.forEach((user) => {
console.log(user.toLowerCase()); // user の型は推論される
});
型注釈を明示すべき場面
型推論では意図が伝わらない場合や、推論結果が意図と異なる可能性がある場合は、型注釈を明示すべきである。
| 場面 | 説明 |
|---|---|
| 関数のパラメータ | パラメータは推論されないため常に明示が必要 |
| 複雑なオブジェクトの型 | interface または type で型を定義して明示する。
|
| 公開APIの戻り値 | export function calculateTotal(items: Item[]): number のように明示する。 |
| 空配列の初期化 | let items: string[] = [] のように型注釈が必要。 |
any になってしまう場面 |
JSON.parse() の戻り値等は型注釈や型アサーションで対処する。
|
// 関数のパラメータ : 常に明示
function greet(name: string, age: number): string {
return `${name} is ${age} years old.`;
}
// 公開 API : 戻り値も明示
export function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// 空配列 : 型注釈が必要
const tags: string[] = [];
// JSON.parse : 型注釈で意図を明示
const data: UserData = JSON.parse(responseText);
判断基準のまとめ
型推論と型注釈の使い分けについて、下表の基準を参考にすると判断しやすい。
| 場面 | 推奨 | 理由 |
|---|---|---|
| 変数初期化 (値が明らか) | 推論に任せる | 冗長な注釈を避け、コードを簡潔に保つ。 |
| 関数パラメータ | 常に明示 | 推論されないため必須 |
| 関数戻り値 (単純) | 推論に任せる | return文から自明な場合 |
| 関数戻り値 (公開 API) | 明示を推奨 | APIの契約として型を明示する。 |
| 複雑なオブジェクト | 明示を推奨 | interface / type で自己文書化する。 |
| 空配列 | 常に明示 | 推論できないため必須 |
| コールバック引数 | 推論に任せる | コンテキスト型推論が機能する。 |
判断の基本方針をまとめると以下の通りである。
- コードの意図が一目で明確 -> 型推論に任せて簡潔に記述する。
- 複雑さが増す、または公開される -> 型注釈で意図を明示して自己文書化する。
- 推論結果が期待と異なる可能性がある -> 型注釈 または
as constで制御する。
関連情報
- TypeScriptの基礎 - 開発環境
- TypeScriptの基礎 - tsconfig.json
- TypeScriptの基礎 - 型注釈とプリミティブ型
- TypeScriptの基礎 - 型の絞り込み : (型ナローイングの詳細)
- TypeScriptの基礎 - 型アサーション : (as const の関連)