動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# の is 感覚で詰まる TypeScript の型ガード — typeof / instanceof / in / ユーザー定義で union を絞る

バイブス父さん
現役の業務SE
2026年9月18日13 min read広告 (PR) を含む場合があります
C# の is 感覚で詰まる TypeScript の型ガード — typeof / instanceof / in / ユーザー定義で union を絞る

is 型パターンや switch のパターンマッチングを、息をするように使ってきた C# 出身者。その人が TypeScript の union 型を絞り込もうとすると、地味に手が止まる。

「あれ、x is Circle みたいに書けんの?? as でキャストすればいけるやろ…って、絞り込めてなくない??」

どうも、ヒロポンです。

今回は、C# の「型で絞る」感覚を持ってる人向け。TypeScript の型ガード(typeof / instanceof / in / ユーザー定義)で union を絞り込む書き方を、対応表とコードで整理します。C# の資産はほぼそのまま効きます。効くんだけど、入り口の感覚が1個だけズレてる。そこを先に潰すと、union で迷わなくなる。

俺が TypeScript の union で最初に固まった話

C# なら、こう書けば型が絞れますよね。

// C# 7.x の is 型パターン。判定と変数束縛が同時に走る
object shape = GetShape();
if (shape is Circle c)
{
    var area = Math.PI * c.Radius * c.Radius; // c は Circle として使える
}

この感覚のまま TypeScript で union を受け取って、俺は最初 as でキャストしたんですよ。shape as Circle って。

そしたら、Square が来ても素通りした。実行時に何のエラーも出ない。radiusundefined のまま計算されて、NaN が返ってくる。しばらく「なんで?」って画面眺めてました。

原因は、C# の as と TypeScript の as が別物だったこと。ここが第一の壁です。

  • C# の as … 実行時に型チェックして、ダメなら null
  • TypeScript の as … コンパイラへの「これはこの型だと思え」という指示だけ。実行時はノーチェック

TS の as は絞り込み(narrowing)じゃない。ただの型アサーションです。だから TS では、絞り込み専用の道具=型ガードを使う。

C# の「型で絞る」と TS の「型ガードで絞る」対応マップ

まず全体像。C# で何を使ってたかで、TS 側の道具が決まります。

C#の型絞り込み(is intパターン/is クラスパターン/switchパターンマッチ/独自判定)とTypeScriptの型ガード(typeof/instanceof/in演算子/ユーザー定義型ガード)の対応表

一番下の「キャスト」行だけ、感覚が逆。ここが C# 出身者の落とし穴です。では上から順に対応を見ていきます。

対応1: プリミティブは typeof で絞る(C# の is int 相当)

数値・文字列・真偽値みたいなプリミティブは、typeof で絞ります。C# の is int / is string に相当する道具ですね。

function format(value: string | number): string {
  if (typeof value === "number") {
    // ここでは value は number に確定している
    return value.toFixed(2);
  }
  // ここまで来たら value は string に絞られている
  return value.trim();
}

console.log(format(3.14159)); // "3.14"
console.log(format("  hi ")); // "hi"

typeof で string|number を絞る実行結果。format(3.14159)が3.14、format("  hi ")がhiになる

こんな感じで、if の中に入ると value の型が勝手に絞られます。TS が「typeof が number なら、このブロックでは number だな」と追ってくれる。これを制御フロー解析と呼びます。

typeof で判定できるのは "string" / "number" / "boolean" / "undefined" / "function" / "object" / "symbol" / "bigint" の8種類だけ。プリミティブ専用、と覚えておけば十分です。

対応2: クラスは instanceof で絞る(C# の is MyClass 相当)

クラスのインスタンスは instanceof で絞ります。C# の is MyClass c に一番近い書き方。

class Dog {
  bark(): string { return "ワン"; }
}
class Cat {
  meow(): string { return "ニャー"; }
}

function speak(animal: Dog | Cat): string {
  if (animal instanceof Dog) {
    return animal.bark(); // animal は Dog に確定
  }
  return animal.meow();   // ここでは Cat
}

console.log(speak(new Dog())); // "ワン"

instanceof でクラスを絞る実行結果。speak(new Dog())がワンを返す

こんな感じでクラスを見分けます。ここで踏みがちなのが、typeof animal === "Dog" と書いてしまうパターン。さっき書いたとおり、typeof はクラスに対して常に "object" を返す。だからクラスの種類は判定できません。クラスは instanceof、と切り替えます。

対応3: プロパティの有無は in で絞る

interfacetype で定義したプレーンなオブジェクト(クラスじゃないやつ)だと、instanceof使えません。実行時に型情報が消えてるからです。

こういう時は in 演算子。「このプロパティ持ってる?」を見て絞ります。

interface Admin {
  role: string;
  permissions: string[];
}
interface Guest {
  role: string;
  visitCount: number;
}

function describe(user: Admin | Guest): string {
  if ("permissions" in user) {
    // permissions を持つ = Admin に確定
    return `管理者:権限 ${user.permissions.length} 個`;
  }
  return `ゲスト:来訪 ${user.visitCount} 回`;
}

いい感じに絞れます。C# だと interface の判定も is IAdmin で書けるので、ここは「TS は構造で見るからプロパティで判定するのか」と発想を切り替える所ですね。

対応4: 独自ルールの判定は「ユーザー定義型ガード」で絞る

判定ロジックが複雑で、関数に切り出したい時。ここで boolean を返す普通の関数を書くと、絞り込みが効きません

戻り値の型を boolean ではなく 引数 is 型 と書く。これがユーザー定義型ガードです。C# の is パターンに一番近い感覚。

interface Admin {
  role: string;
  permissions: string[];
}
interface Guest {
  role: string;
  visitCount: number;
}

// 戻り値を「user is Admin」にするのがミソ
function isAdmin(user: Admin | Guest): user is Admin {
  return "permissions" in user;
}

function handle(user: Admin | Guest): void {
  if (isAdmin(user)) {
    // isAdmin が true を返したので、ここでは user は Admin
    console.log(user.permissions.join(", "));
  }
}

ん?普通に boolean 返すんじゃダメなん?って思いますよね。ダメなんです。戻り値を boolean で書くと、if (isAdmin(user)) の中でも userAdmin | Guest のまま。user is Admin と書いて初めて、TS が「この関数が true なら Admin だ」と信じてくれます。

ミニマム検証: union を1つずつ絞ってみる

手を動かして確かめたい人向け。ファイル1個で動きます。

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;

function area(shape: Shape): number {
  // kind というリテラルで絞る(判別可能なユニオン)
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2; // shape は Circle
  }
  return shape.side ** 2;               // shape は Square
}

console.log(area({ kind: "circle", radius: 2 })); // 12.566...
console.log(area({ kind: "square", side: 3 }));   // 9

判別可能なユニオンで絞る実行結果。円の面積12.566と正方形の面積9が出力される

kind みたいな共通の目印プロパティ(リテラル型)を1個持たせておく。すると shape.kind === "circle" で一発で絞れます!!これが判別可能なユニオンで、TS で一番よく使う絞り込みです。C# の switch パターンマッチングに近い使い勝手ですね。

ハマりポイント: C# の as 感覚が一番危ない

C# 出身者が実際に踏むやつを、危ない順に3つ。

as で絞ったつもりになる(最危険)

冒頭の俺のミスです。C# の as は実行時チェックがあるけど、TS の as は無い。shape as Circle は嘘をつけてしまう。間違った型を通しても実行時まで気づけません。絞りたいなら as じゃなく型ガード、が鉄則。

TSのasで絞ったつもりで実行時チェックが無く別の型が素通りする危険な書き方と、型ガードでunionを安全に絞り込む書き方の差分

typeof でクラスを判定しようとする

typeof instance === "Dog" は、まず当たりません。クラスは全部 "object"。クラスの判定は instanceof に切り替える、で解決します。

③ interface に instanceof を使おうとする

interfacetype は実行時に消えます。だから user instanceof Admin はコンパイルエラー(Admin は値じゃない、と怒られる)。プレーンなオブジェクトは in かユーザー定義型ガードで絞る。ここを混同すると、30分は溶けます。

俺の現場メモ: 型ガードで詰まらないコツ

TypeScript の案件に初めて入った時。C# の as のノリで書いて、レビューで「それ絞れてないですよ」って指摘されまくったんですよ。動くけど型安全じゃない、という状態。

そこから掴んだコツは、「絞り込みたい対象の正体で道具を選ぶ」の一点です。

  • プリミティブ → typeof
  • クラス → instanceof
  • プレーンなオブジェクト → in かユーザー定義型ガード

この対応さえ頭に入っていれば、union が来ても「これはクラスだから instanceof だな」と手が動く。逆に、C# の is / as を1個の道具で全部やろうとすると、たいていどこかで詰まります。

あと地味に効いたのが、判別可能なユニオン(kind プロパティ)を最初から設計に入れておくこと。後から型ガードを書きまくるより、目印を1個持たせるほうが圧倒的にラクでした。

まとめ

C# の型絞り込み経験は、TypeScript でもほぼそのまま効きます。ズレるのは入り口の1点だけ。

  • as は絞り込みじゃない(C# と違い実行時チェックなし)。絞るなら型ガード
  • プリミティブtypeof
  • クラスinstanceof
  • プレーンなオブジェクトin かユーザー定義型ガード(x is Foo
  • 設計段階で 判別可能なユニオンkind)を持たせると一番ラク

「どの型か分からない」で止まってた union が、道具の対応表を1枚持っておくだけでスッと絞れるようになります。

よくある質問

TypeScript の as は C# の as と同じですか?

違います。C# の as は実行時に型チェックして、失敗すると null を返します。TypeScript の as は「コンパイラに型をこう見なせ」と指示するだけの型アサーションで、実行時には何もチェックしません。絞り込みにはならないので、型ガードを使います。

typeof でクラスのインスタンスを判定できますか?

できません。typeof はクラスのインスタンスに対して常に "object" を返すため、どのクラスかは区別できません。クラスの判定には instanceof を使います。

instanceof が使えないのはどんな時ですか?

interfacetype エイリアスは実行時に消えるため、instanceof で判定できません。この場合はプロパティの有無を見る in 演算子か、ユーザー定義型ガード(x is Foo)を使います。

ユーザー定義型ガードの「x is Foo」とは何ですか?

関数の戻り値の型を boolean ではなく 引数 is 型 と書くことで、その関数が true を返したら呼び出し側で型が絞り込まれる、とコンパイラに教える仕組みです。C# の is パターンに一番近い書き方です。

次に読むべき記事

以上!

同じ「as で絞れると思ってた」人がいたら、この記事シェアしてくれると嬉しいです!!


執筆者

バイブス父さん — 業務 SE 7 年(SIer 正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SIer の正社員からフリーランスに転じ、複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について