「型の絞り込み」とは何かを一言でつかむ
型の絞り込み(narrowing)は、
「最初はざっくりした型だったものを、条件分岐などを通して“より具体的な型”に狭めていくこと」です。
TypeScript では、よくこんな型が出てきます。
let value: string | number;
TypeScriptこの時点では、value が string のときもあれば number のときもあります。
でも、コードの中で
「もし文字列ならこうする」
「数値ならこうする」
と分けて書きたいですよね。
そのときに、
if の条件を読んで
「ここでは string だけ」「ここでは number だけ」
と、TypeScript が“型を狭く理解してくれる”仕組み。
これが「型の絞り込み」です。
ふつうに if や === を書いているようでいて、
実は「そのたびに、変数の型も一緒に変わっている」。
その感覚を、例を通してはっきりさせていきます。
一番基本:typeof での型の絞り込み
string | number を if で分けて扱う
まず、超ベーシックなパターンから。
function printFormatted(value: string | number) {
if (typeof value === "string") {
console.log("文字列:", value.toUpperCase());
} else {
console.log("数値:", value.toFixed(2));
}
}
TypeScriptここでの流れを「型の視点」で追います。
関数に入った時点では、value の型は string | number(どちらか)。if (typeof value === "string") と書いた瞬間、
その if ブロックの中では、TypeScript は
「ここでは value を string として扱っていい」
と理解します。
だから、value.toUpperCase() や value.length がエラーなく呼べます。
逆に else の中では、
「ここに来るということは、さっきの条件(string)が false だった」
→ 「じゃあここでは string ではなく number のはず」
と判断されて、value.toFixed(2) が安全になります。
ここで起きているのがまさに 「型の絞り込み」 です。
最初:string | number(広い)
if の中:string に狭まる
else の中:number に狭まる
というふうに、「条件によって型が細かくなっている」わけです。
null を含む型の絞り込み(nullチェック)
string | null から string に絞る
次によく出てくるのが、null を含む型です。
function greet(name: string | null) {
// console.log(name.toUpperCase()); // エラー
}
TypeScriptname が null かもしれないので、そのまま toUpperCase は危険です。
ここで「nullチェック」が型の絞り込みとして効いてきます。
function greet(name: string | null) {
if (name === null) {
console.log("名前がありません");
return;
}
console.log("こんにちは、" + name.toUpperCase());
}
TypeScriptこのコードでは、
関数の入り口:name は string | nullif (name === null) の中:name は null に絞られる(だから何も呼べない)
if のあと(return の下):null の場合はすでに return した
→ ここまで来たということは name は string に絞られている
となります。
この「null じゃない世界だけを残す」書き方は、実務で超よく使います。
型の絞り込みは、“異常系を先に処理して、残りを正常系だけにする”という設計と非常に相性がいいです。
オブジェクトの型の絞り込み(in 演算子)
union 型のどのパターンかをプロパティで見分ける
次はオブジェクトの union 型を扱ってみます。
type User = {
name: string;
};
type Admin = {
name: string;
permissions: string[];
};
type Person = User | Admin;
TypeScriptPerson は User か Admin のどちらかです。Admin だけが permissions を持っています。
これを in 演算子で絞り込みます。
function printPerson(person: Person) {
if ("permissions" in person) {
console.log("管理者:", person.name, person.permissions);
} else {
console.log("一般ユーザー:", person.name);
}
}
TypeScriptここでの絞り込みはこうです。
最初:person は User | Adminif ("permissions" in person) の中:
→ permissions プロパティを持つのは Admin だけ
→ ここでは Admin 型に絞り込まれる
else の中:
→ 残りは User 型に絞り込まれる
つまり、「このプロパティを持っているか?」という条件が、そのまま「どの型か?」の判定になっているわけです。
in での絞り込みは、
どの型がどのプロパティを持っているか
という設計とセットで考えると、とても使いやすくなります。
判別可能な union と絞り込み(status / kind / type で分ける)
状態のバリエーションを型で表し、if で絞る
よくあるパターンを見てみましょう。
type LoadingState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; error: string };
TypeScriptこの LoadingState は、
待機中
読み込み中
成功(data を持つ)
失敗(error を持つ)
という4パターンを持つ union 型です。
これを if で絞り込みつつ使うと、こうなります。
function render(state: LoadingState) {
if (state.status === "idle") {
console.log("待機中");
} else if (state.status === "loading") {
console.log("読み込み中…");
} else if (state.status === "success") {
console.log("成功:", state.data);
} else {
console.log("失敗:", state.error);
}
}
TypeScript型の絞り込みとしてはこう動いています。
state.status === "idle" の中
→ state は { status: "idle" } に絞られる
"loading" の中
→ { status: "loading" } に絞られる
"success" の中
→ { status: "success"; data: string } に絞られる(だから state.data が使える)
最後の else
→ 残りの { status: "error"; error: string } に絞られる(だから state.error が使える)
「status の値によって、state の型のバリエーションを切り分ける」
これが判別可能な union と呼ばれるパターンで、型の絞り込みとの相性が最高にいいです。
ユーザー定義型ガードによる絞り込み
自分で「この関数を通ったら型をこう見なしていい」と教える
もう一段進んだ絞り込みの方法が、ユーザー定義型ガードです。
type Person = User | Admin;
function isAdmin(person: Person): person is Admin {
return "permissions" in person;
}
TypeScriptここでのポイントは、戻り値の型 person is Admin。
これが「もし true を返したなら、この person は Admin として扱っていいよ」という約束になります。
これを使うと、if の中で型が絞られます。
function printPermissions(person: Person) {
if (isAdmin(person)) {
// ここでは person は Admin 型
console.log(person.permissions);
} else {
// ここでは person は User 型
console.log(person.name);
}
}
TypeScriptif (isAdmin(person)) の中では、TypeScript は
「isAdmin の戻り値が true なら、person は Admin のはず」
と理解し、person を Admin 型に絞ってくれます。
ここで起きている絞り込みは、
関数の外側では Person
型ガード関数の判定を通ったブロックでは Admin
という「型の変化」です。
「この関数が true を返したら、引数の型をこう絞っていい」と宣言するのがユーザー定義型ガード
という感覚で捉えてみてください。
型の絞り込みを設計として捉える
if を書くたびに「今この変数は何型のつもりで書いているか」を意識する
型の絞り込みに慣れるために、if や switch を書くとき、
必ず自分に問いかけてみてほしいことがあります。
この条件が true のとき、この変数はどの型だと思ってコードを書いている?
この条件が false のときは、どの型だと思っている?
例えば、
function handle(value: string | number | null) {
if (value === null) {
// 「ここでは“値なし”のケースだけ扱っている」のだな
} else if (typeof value === "string") {
// 「ここでは string のケースだけ扱っている」のだな
} else {
// 残りは number のケースだけ
}
}
TypeScriptこの感覚をはっきりさせると、
TypeScript の型エラーが「うるさい警告」ではなく、
「あなたが今ここを string だと思って書いてるけど、実は number の可能性もあるよ?」
と教えてくれる“会話”に変わります。
型の絞り込みは、
TypeScript が勝手にやっている魔法ではなく、
「あなたが if で書いた“場合わけの意図”を、型として理解してくれている結果」です。
だからこそ、if や switch を書くときは、
この分岐で、どの世界とどの世界を分けたいんだっけ?
を意識することが、narrowing を自分の武器にする一番の近道になります。
まとめ:型の絞り込みは「曖昧な世界から、はっきりした世界を切り出す」作業
最後に、感覚だけ整理します。
union 型や unknown、string | null のような型は、
最初は「いろんな可能性が混ざっている“曖昧な世界”」です。
型の絞り込み(narrowing)は、
typeof / null チェック / in / === / ユーザー定義型ガード
といった条件を使って、
「ここから先はこのパターンだけ」
「ここではもう null じゃない」
「ここでは Admin だけ」
という 「はっきりした世界」を切り出していく作業 です。
その世界の中では、
その型にだけ存在するプロパティやメソッドを、安心して使うことができます。
コードを書いていて、
「本当はこの if の中では、もうこの型のはずなんだけどな」
と感じたら、そこが「型の絞り込み」を意識するポイントです。
その「はず」を、TypeScript にも分かってもらう。
それができるようになると、
型はあなたを縛るものではなく、あなたの意図を守ってくれる相棒になっていきます。
