TypeScript | 基礎文法:Union・基本型操作 – 型の絞り込み(narrowing)

TypeScript
スポンサーリンク

「型の絞り込み」とは何かを一言でつかむ

型の絞り込み(narrowing)は、
「最初はざっくりした型だったものを、条件分岐などを通して“より具体的な型”に狭めていくこと」です。

TypeScript では、よくこんな型が出てきます。

let value: string | number;
TypeScript

この時点では、valuestring のときもあれば 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()); // エラー
}
TypeScript

namenull かもしれないので、そのまま toUpperCase は危険です。
ここで「nullチェック」が型の絞り込みとして効いてきます。

function greet(name: string | null) {
  if (name === null) {
    console.log("名前がありません");
    return;
  }

  console.log("こんにちは、" + name.toUpperCase());
}
TypeScript

このコードでは、

関数の入り口:namestring | null
if (name === null) の中:namenull に絞られる(だから何も呼べない)
if のあと(return の下):null の場合はすでに return した
→ ここまで来たということは namestring に絞られている

となります。

この「null じゃない世界だけを残す」書き方は、実務で超よく使います。
型の絞り込みは、“異常系を先に処理して、残りを正常系だけにする”という設計と非常に相性がいいです。


オブジェクトの型の絞り込み(in 演算子)

union 型のどのパターンかをプロパティで見分ける

次はオブジェクトの union 型を扱ってみます。

type User = {
  name: string;
};

type Admin = {
  name: string;
  permissions: string[];
};

type Person = User | Admin;
TypeScript

PersonUserAdmin のどちらかです。
Admin だけが permissions を持っています。

これを in 演算子で絞り込みます。

function printPerson(person: Person) {
  if ("permissions" in person) {
    console.log("管理者:", person.name, person.permissions);
  } else {
    console.log("一般ユーザー:", person.name);
  }
}
TypeScript

ここでの絞り込みはこうです。

最初:personUser | Admin
if ("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);
  }
}
TypeScript

if (isAdmin(person)) の中では、TypeScript は

「isAdmin の戻り値が true なら、person は Admin のはず」

と理解し、personAdmin 型に絞ってくれます。

ここで起きている絞り込みは、

関数の外側では 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 型や unknownstring | null のような型は、
最初は「いろんな可能性が混ざっている“曖昧な世界”」です。

型の絞り込み(narrowing)は、

typeof / null チェック / in / === / ユーザー定義型ガード

といった条件を使って、

「ここから先はこのパターンだけ」
「ここではもう null じゃない」
「ここでは Admin だけ」

という 「はっきりした世界」を切り出していく作業 です。

その世界の中では、
その型にだけ存在するプロパティやメソッドを、安心して使うことができます。

コードを書いていて、

「本当はこの if の中では、もうこの型のはずなんだけどな」

と感じたら、そこが「型の絞り込み」を意識するポイントです。
その「はず」を、TypeScript にも分かってもらう。

それができるようになると、
型はあなたを縛るものではなく、あなたの意図を守ってくれる相棒になっていきます。

タイトルとURLをコピーしました