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 が来ても素通りした。実行時に何のエラーも出ない。radius が undefined のまま計算されて、NaN が返ってくる。しばらく「なんで?」って画面眺めてました。
原因は、C# の as と TypeScript の as が別物だったこと。ここが第一の壁です。
- C# の
as… 実行時に型チェックして、ダメならnull - TypeScript の
as… コンパイラへの「これはこの型だと思え」という指示だけ。実行時はノーチェック
TS の as は絞り込み(narrowing)じゃない。ただの型アサーションです。だから TS では、絞り込み専用の道具=型ガードを使う。
C# の「型で絞る」と TS の「型ガードで絞る」対応マップ
まず全体像。C# で何を使ってたかで、TS 側の道具が決まります。

一番下の「キャスト」行だけ、感覚が逆。ここが 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"

こんな感じで、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())); // "ワン"

こんな感じでクラスを見分けます。ここで踏みがちなのが、typeof animal === "Dog" と書いてしまうパターン。さっき書いたとおり、typeof はクラスに対して常に "object" を返す。だからクラスの種類は判定できません。クラスは instanceof、と切り替えます。
対応3: プロパティの有無は in で絞る
interface や type で定義したプレーンなオブジェクト(クラスじゃないやつ)だと、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)) の中でも user は Admin | 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

kind みたいな共通の目印プロパティ(リテラル型)を1個持たせておく。すると shape.kind === "circle" で一発で絞れます!!これが判別可能なユニオンで、TS で一番よく使う絞り込みです。C# の switch パターンマッチングに近い使い勝手ですね。
ハマりポイント: C# の as 感覚が一番危ない
C# 出身者が実際に踏むやつを、危ない順に3つ。
① as で絞ったつもりになる(最危険)
冒頭の俺のミスです。C# の as は実行時チェックがあるけど、TS の as は無い。shape as Circle は嘘をつけてしまう。間違った型を通しても実行時まで気づけません。絞りたいなら as じゃなく型ガード、が鉄則。

② typeof でクラスを判定しようとする
typeof instance === "Dog" は、まず当たりません。クラスは全部 "object"。クラスの判定は instanceof に切り替える、で解決します。
③ interface に instanceof を使おうとする
interface や type は実行時に消えます。だから 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 が使えないのはどんな時ですか?
interface や type エイリアスは実行時に消えるため、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 で経歴詳細を見る




