型推論とは、書かなかった型をTypeScriptが導き出すことです。変数を初期化すればその値の型になり、関数から値を返せば戻り値の型もそれに従います。多くのTypeScriptのコードは、思っているよりずっと少ない型注釈で書かれています。
コメントは、エディターで各名前にカーソルを合わせたときに表示される型です。最後の行は、count に型注釈を書いていないにもかかわらずコンパイルエラー(TS2322)になります。// @ts-expect-error でそのエラーを想定済みとしているので、ブロックの残りは実行されます。
let と const: リテラル型の拡大
const は変更できないので、TypeScriptはいちばん狭い型、つまりリテラルの値そのものを型にします。let はあとで再代入されるかもしれないので、広い型になります。
let a = "left"; // string
const b = "left"; // "left"
let c = 42; // number
const d = 42; // 42
const e = true; // true
これは、特定の値しか受け付けない関数に渡すときに問題になります:
index.ts(7,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.
const を使うか、変数に型注釈を書けば直ります: let dir: "left" | "right" = "left"。リテラル型については専用のページ リテラル型 があります。
オブジェクトと配列はメンバーが拡大される
オブジェクトリテラルのプロパティは変更可能なので、オブジェクト自体が const でも let と同じように拡大されます。配列の要素も同じです。as const を付けるとすべてが readonly になり、リテラル型が保たれます。
戻り値の型推論
関数の戻り値の型は return 文から推論されます。return が複数ある場合は、それらのユニオン型になります。
ほとんどの関数は推論された戻り値の型で問題ありません。シグネチャを契約にしたいエクスポートする関数や公開する関数では、戻り値の型を明示します。そうすれば、本体の変更で違う型を返すようになったとき、どこか離れた呼び出し側ではなくその関数の場所でエラーになります。
文脈的型付け: コールバックは型を受け取る
パラメーターの型は関数の呼び出し方からは推論されません。例外は2つあります。デフォルト値を持つパラメーターはその値の型になります(function retry(times = 3) なら times は number)。また、関数型が期待されている場所に書いた関数は、その文脈からパラメーターの型を受け取るので、コールバックのパラメーターには型注釈が要りません。
forEach、filter、reduce、Promise のコールバック、イベントハンドラー、そして関数型が宣言されたパラメーターに渡すあらゆる関数で同じことが起こります。
パラメーターには型注釈が必要
デフォルト値もこうした文脈もない場合、TypeScriptは関数の呼び出し方を見てパラメーターの型を推測したりはしません。strict が有効なら、型注釈のないパラメーターはエラー TS7006 になります:
index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.
function double(x: number) と書けばブロックは 42 を出力し、戻り値の型は x * 2 から number と推論されます。
最適共通型
異なる種類の値を含む配列リテラルでは、TypeScriptはそれらの型のユニオンの配列を推論します。書いていない共通の基底型を探すことはしません。
注意が必要なのは空の配列です。const list = [] は「進化する」配列として始まり、push するたびに型が広がります。const list: string[] = [] と型注釈を書けば意図が最初から明確になり、読みやすくなります。
型注釈を書くべきとき
| 状況 | 型注釈を書く? |
|---|---|
| 値で初期化する変数 | 不要。値が型を決めます |
値なしで宣言する変数(let result;) | 必要。let result: string; |
| 関数のパラメーター | 必要(文脈のあるコールバックやデフォルト値がある場合を除く) |
| 小さな内部関数の戻り値の型 | どちらでもよい |
| エクスポートする関数や公開する関数の戻り値の型 | 契約として書くことを推奨 |
| あとで中身を入れる空の配列やオブジェクト | 必要。const ids: number[] = [] |
| 値のリテラル型を保ちたい | as const を使う |
| 値をある型に合わせつつ、推論された型を保ちたい | satisfies を使う |
推論された型をいちばん手早く確認するには、エディターで名前にカーソルを合わせます。エディターがない場合は、const probe: boolean = mixed; のように値を違う型の変数に代入すれば、コンパイラーのエラーメッセージに推論された型が表示されます。
よくある質問
TypeScriptの型推論とは何ですか?
書かなかった型をコンパイラーが導き出すことです。let count = 0 なら count の型は number になり、2つの数値の a + b を返す関数の戻り値の型は number になります。推論された型は、書いた型とまったく同じようにチェックされます。
TypeScriptではすべての変数に型注釈を書くべきですか?
いいえ。型注釈を書くのは、関数のパラメーター(呼び出し方からは推論されず、デフォルト値かコールバックの文脈からだけ推論されます)、APIを安定させたい場合のエクスポートする関数の戻り値、値なしで宣言する変数です。それ以外は推論に任せます。const name: string = "Ada" はコンパイラーがすでに知っていることを繰り返しているだけです。
なぜ const はリテラル型に、let は string に推論されるのですか?
const は再代入できないので、const dir = "left" はリテラル型 "left" になります。let はあとでほかの文字列を持つかもしれないので、let dir = "left" は string に拡大されます。オブジェクトリテラルのプロパティも再代入できるため同じように拡大されます。as const で拡大を止められます。
TypeScriptで「パラメーターが暗黙的に 'any' 型になる」と言われるのはなぜですか?
strict に含まれる noImplicitAny によるエラー TS7006 です。TypeScriptは関数の呼び出し方からパラメーターの型を推論しないので、型のないパラメーターは any になってしまいます。function double(x: number) のように型注釈を書くか、map に渡すコールバックのように文脈から型がわかる場所に関数を渡します。
TypeScriptが推論した型を確認するには?
エディターで名前にカーソルを合わせます(VS Codeなど多くのエディターは推論された型をツールチップに表示します)。コード上では、その値を明らかに違う型の変数に代入すると、コンパイラーがエラーメッセージに推論された型を表示します。