型の絞り込み(narrowing)とは、それまでにコードが行ったチェックに基づいて、TypeScriptがコードのある地点での値の型をより具体的に割り出すことです。string | number 型の引数は、if (typeof x === "string") の中では string に、else の中では number になります。
チェックの前に value.toFixed(2) を呼ぶとコンパイルエラーになります。string には toFixed がないからです。チェックは実行時に動く普通のJavaScriptで、絞り込みはコンパイラーがそれを読んで型を調整することです。余分なコードは何も出力されません。
制御フロー分析
TypeScriptは関数の中のすべての経路をたどります。if/else、早期の return と throw、switch、ループ、そして短絡評価の演算子 &&、||、??、?: です。各地点での変数の型は、そこでまだありうる型です。
早期 return のスタイル(ガード節)は、いちばん読みやすい絞り込み方です。特殊なケースを先に処理すれば、関数の残りはきれいな型で書けます。
絞り込みの方法一覧
| 形 | 例 | 絞り込む対象 |
|---|---|---|
| typeof | typeof x === "string" | プリミティブと関数 |
| truthiness | if (x) | null、undefined、falsy なリテラルを除く |
| 等価比較 | x === "a"、x == null、x !== undefined | リテラル、null、undefined |
in | "swim" in pet | プロパティによるオブジェクトのユニオン型 |
| instanceof | err instanceof TypeError | クラスのインスタンス |
Array.isArray | Array.isArray(x) | 配列とそれ以外 |
| 代入 | x = 5 | 代入した値の型 |
| 型述語 | function isUser(x: unknown): x is User | チェックできるものなら何でも |
| アサーション関数 | function assertUser(x: unknown): asserts x is User | 呼び出し以降のすべて |
| 判別プロパティ | switch (shape.kind) | タグ付きユニオン型 |
最後の3つは型ガードと判別可能なユニオン型のページで扱います。残りは以下で説明します。
truthiness による絞り込み
if (x) は null と undefined(と、リテラル型の false、0、"")を取り除きます。短く書けますが、典型的な落とし穴が1つあります。0 と "" は falsy なので、正しい値が「ない」ものとして扱われてしまいます。
数値と文字列は、undefined や null と明示的に比較しましょう(または ?? を使います)。オブジェクト、配列、関数は falsy になることがないので、truthiness で問題ありません。
等価比較による絞り込み
===、!==、==、!= は両辺を絞り込みます。リテラルと比較するとそのリテラルに絞り込まれます。== null(緩い等価比較)は1回のチェックで null と undefined の両方に一致し、緩い等価比較が慣用的に使われる唯一の場所です。
2つの変数を比較すると、両方が共通して取りうる型に絞り込まれます。a: string | number と b: string | boolean が a === b を満たすなら、if の中ではどちらも string です。
in 演算子
"key" in obj は、オブジェクト型のユニオンを、そのプロパティを持つ(または持つかもしれない)メンバーに絞り込みます。
in はオブジェクトであることがわかれば unknown にも使えます。typeof v === "object" && v !== null && "id" in v のあとでは、v が unknown 型の id プロパティを持つことをTypeScriptは知っています。自分で設計するユニオン型なら、メソッドの有無を調べるよりも共通のタグプロパティ(kind: "fish")を使うほうがわかりやすくなります。このパターンを判別可能なユニオン型といいます。
代入による絞り込み
変数には宣言された型と、代入に応じて変わる絞り込まれた型があります。値を代入すると、宣言された型の範囲内で、その値の型に絞り込まれます。
絞り込みが外れる場面
絞り込みは局所的で慎重です。次のような場面では絞り込みがリセットされます:
- 別の式。
obj.nameをチェックするとobj.name(とobj["name"])は絞り込まれますが、keyがリテラルではなくstring型の変数の場合のobj[key]や、チェックの前に作ったコピーは絞り込まれません。 - コールバックと再代入。 コールバックの中では、絞り込まれた
letは、コールバックを作ったあとに再代入されない場合にだけ絞り込みが保たれます。constや再代入されない引数は絞り込まれたままです。 - ヘルパーに隠れたチェック。
isString(x: unknown): booleanという関数はコンパイラーに何も伝えません。戻り値の型を型述語x is stringにすれば、その呼び出しはtypeofと同じように絞り込みます。
コンパイラーは index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. を報告します。コールバックは x = undefined のあとで実行されるかもしれないからです。最後の代入を消せば(または if の中で値を const にコピーすれば)コンパイルでき、5 が2回出力されます。
boolean を返すヘルパーは、それが何を保証するかを宣言すれば直せます。それが型ガードです:
function isString(value: unknown): value is string {
return typeof value === "string";
}
TypeScript 5.5 以降、コンパイラーは単純なアロー関数からこうした型述語を推論します。list.filter((x) => x !== undefined) が undefined を含まない配列を返すようになったのはこのためです。
よくある質問
TypeScriptの型の絞り込みとは何ですか?
コードが行うチェックに基づいて、TypeScriptがブロックの中の変数の型をより具体的にすることです。if (typeof x === "string") のあとでは、string | number は if の中では string、else の中では number になります。コンパイラーは if、else、return、switch、&&、||、?: をたどって各地点の型を割り出します。これを制御フロー分析といいます。
TypeScriptで型が絞り込まれないのはなぜですか?
よくある原因は次のとおりです。チェックした式と使っている式が違う(obj.a をチェックして、string 型の key で obj[key] を使っている)。コールバックを作ったあとに let を再代入しているので、コールバックの中で絞り込みが外れる。チェックが型述語 x is T ではなく普通の boolean を返すヘルパーの中に隠れている。
型の絞り込みは実行時に動きますか?
チェックは動きます。typeof、instanceof、in、=== は実行される普通のJavaScriptです。絞り込み自体はコンパイル時だけのものです。TypeScriptは実行時のチェックを読んで静的な型をそれに合わせるだけで、出力されるJavaScriptには何も追加されません。
TypeScriptで unknown 型を絞り込むには?
同じチェックを使います: typeof value === "string"、Array.isArray(value)、value instanceof Date、オブジェクトなら typeof value === "object" && value !== null && "id" in value。再利用するチェックは、戻り値の型が value is T の型ガード関数にします。
TypeScriptで配列から undefined を取り除くには?
TypeScript 5.5 以降では、コールバックが型述語として推論されるので、items.filter((x) => x !== undefined) は undefined を含まない T[] を返します。それより古いバージョンでは型述語を自分で書きます: items.filter((x): x is T => x !== undefined)。