TypeScriptを書いていて、こんな経験はありませんか?

ユニオン型を受け取ったら、ifで分岐したのに型エラーが消えない…
仕方なくasでキャストしているけど、なんだか気持ち悪い…
typeofinstanceof、どっちを使えばいいかいつも迷う…

そんなときに使うのが、TypeScriptの型ガードです。型ガードを使えば、asに頼らずユニオン型を安全に絞り込めるようになります。組み込みの4種類(typeofinstanceofin/リテラル等価)と、自分で書くユーザー定義型ガード(is演算子)の合計5パターンで、実務の大半をカバーできます。

この記事では、型ガードの仕組みから、4種類の使い分け、is演算子の書き方、そして書く側になったときに踏みやすい落とし穴までを、実例コード付きで順に解説します。

この記事は次のような方におすすめです。

この記事はこんな人におすすめ!
  • ユニオン型を扱い始めて、型エラーの消し方に困っている方
  • typeofinstanceofinの使い分けを整理したい方
  • ユーザー定義型ガード(is演算子)の書き方を知りたい方
  • asを使わずに型安全なコードを書きたい方
  • 外部APIのレスポンスを型安全に扱う方法を探している方

読み終えるころには、ユニオン型やunknownを受け取っても落ち着いて型を絞り込めるようになり、asに頼らない設計の引き出しが増えているはずです。

それでは、順を追って詳しく見ていきましょう!

まずは動画解説を観る

Contents
  1. TypeScriptの型ガードとは|ユニオン型を絞り込む仕組み
  2. TypeScriptの型ガード4種類|組み込みの判定方法
  3. ユーザー定義型ガードとis演算子の書き方
  4. typeof・instanceof・in・ユーザー定義型ガードの使い分け
  5. 型ガード関数を書くときのアンチパターン
  6. assertsキーワード(アサーション関数)への発展
  7. よくある質問
  8. まとめ|型ガードを使い分けて型安全なコードへ

TypeScriptの型ガードとは|ユニオン型を絞り込む仕組み

まずは「型ガードとは何か」を一言で押さえましょう。TypeScriptで型ガードと呼ばれるのは、コード上の条件分岐から、コンパイラが変数の型を狭く推論し直す仕組みのことです。

たとえばstring | numberというユニオン型を受け取った変数は、そのままでは文字列のメソッドも数値のメソッドも呼べません。両方の可能性があるからです。そこで条件分岐で「いまはstringである」と特定できれば、コンパイラがその分岐の中だけ型をstringとして扱ってくれます。これが型ガードの基本動作です。

ここで重要なのが、TypeScriptの型情報はコンパイル時にしか存在せず、実行時にはすべて消えるという性質です。実行時には素のJavaScriptとして動くため、型を絞り込むには「実行時の値を実際に検査するJavaScriptコード」が必要になります。

つまり型ガードは次の2つの役割を同時に担っています。

  • 実行時:値を検査して、いまの型を判別する(JavaScriptの仕事)
  • コンパイル時:その検査結果をもとに、変数の型を絞り込む(TypeScriptの仕事)

「コンパイル時」と「実行時」は、TypeScriptを理解するうえで欠かせない区別です。コンパイル時とは、.tsファイルを.jsに変換するときのこと。型注釈や型エイリアスはこの段階で使われ、JavaScriptには出力されません。実行時とは、変換後のJavaScriptがブラウザやNode.js上で動くときのこと。実行時には型情報が一切残らないため、「この値がUser型かどうか」を実行時に確かめたいなら、自分で判別コードを書く必要があります。

最小の例で雰囲気をつかんでおきましょう。string | numberを受け取り、文字列なら大文字化、数値なら小数点以下を切り捨てる関数です。

function format(value: string | number): string {
  if (typeof value === "string") {
    // この分岐の中では value は string として扱われる
    return value.toUpperCase();
  }
  // ここに来た時点で value は number に絞り込まれる
  return Math.floor(value).toString();
}

このコードのポイントは次の通りです。

  • typeof value === "string"の分岐内では、コンパイラがvaluestringとして扱うため.toUpperCase()が呼べる
  • ifを抜けた後はvaluenumberに絞り込まれ、Math.floor()を安全に渡せる
  • typeofというJavaScriptの演算子に、TypeScriptが型の絞り込みを連動させている

なお、本記事のサンプルコードはtsconfig.jsonstrict: trueを前提としています。strictを外すと挙動が変わる箇所があるため、実装はstrict: trueで確認してください。

型ガードが必要になる場面

ユニオン型を扱い始めると、まず必ずぶつかるのが「共通プロパティ以外を呼べない」というエラーです。

たとえば次のコードは、コメントの位置でTypeScriptがエラーを出します。

function format(value: string | number): string {
  // エラー: プロパティ 'toUpperCase' は型 'string | number' に存在しません。
  return value.toUpperCase();
}

string | numberには「文字列としても数値としてもありうる」値が入ります。コンパイラは「どちらでも呼べるメソッドしか呼べない」と判断するため、toUpperCaseのような文字列専用メソッドは拒否されます。

これを型ガードで解消すると、次のようになります。

function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // OK: value は string
  }
  return value.toFixed(2); // OK: value は number
}

ポイントはシンプルです。

  • 分岐の前はstring | numberという広い型
  • 分岐の中はstringまたはnumberという狭い型
  • 「狭い型」になってはじめて、その型固有のメソッドを呼べる

「広い型を、安全に狭い型として扱えるようにする」のが型ガードの目的です。

制御フロー解析(control flow analysis)との関係

型ガードを支えているのが、TypeScriptの制御フロー解析という機能です。これは、コードの実行の流れを追いながら、その時点で変数がどの型になっているかをコンパイラが追跡する仕組みです。

次のコードでカーソルをコメント位置に当てると、valueの型がそれぞれ別物になっているのがわかります。

function describe(value: string | number): void {
  let label: string;

  if (typeof value === "string") {
    // ここでは value: string
    label = `文字列: ${value.length}文字`;
  } else {
    // ここでは value: number
    label = `数値: ${value.toFixed(2)}`;
  }

  // 分岐をまたいだここ(合流地点)では value: string | number に戻っている
  console.log(label, value);
}

このコードのポイントは次の通りです。

  • ifの中ではstringに絞り込まれている
  • else側では、stringの可能性が消えるため自動的にnumberに絞り込まれる
  • if/elseを合流したあとの行では、型がまたstring | numberに戻る

ifで確認した側」だけでなく「else側でも反対の型に絞られる」というのは、制御フロー解析がきちんと働いている証拠です。型ガードを書くときは常に、両側で型がどう変わるかを意識すると見通しがよくなります。

TypeScriptの型ガード4種類|組み込みの判定方法

TypeScriptには、JavaScriptの演算子を流用した組み込みの型ガードが4種類あります。それぞれ判定できる対象と書き方が異なるため、まずは全体像を表で押さえましょう。

種類 判定対象 記法 主な限界
typeof プリミティブ型(string/number/boolean/bigint/symbol/undefined/object/function) typeof x === "string" nullも配列も"object"になる
instanceof クラスのインスタンス x instanceof Error プレーンオブジェクトには使えない
in演算子 オブジェクトのプロパティ有無 "swim" in animal プロパティ名が一致する別物を判別できない
リテラル等価比較 判別用プロパティの値 result.kind === "success" タグ付きユニオン設計が前提

まずはこう覚えればOKです。プリミティブ→typeof、クラス→instanceof、プロパティの有無→in、判別タグありのユニオン→等価比較。それぞれの詳細を見ていきましょう。

イメージをつかむために、4種類を並べた最小サンプルを先に見ておきます。

// 1. typeof:プリミティブ
if (typeof value === "string") { /* value: string */ }

// 2. instanceof:クラスインスタンス
if (error instanceof Error) { /* error: Error */ }

// 3. in:プロパティの有無
if ("swim" in animal) { /* animal: Fish */ }

// 4. リテラル等価:判別タグ
if (result.kind === "success") { /* result: { kind: "success", ... } */ }

typeofで判定する|プリミティブ型の絞り込み

typeofはJavaScriptの演算子で、値の種類を文字列として返します。返り値は次の8種類のいずれかです。

  • string
  • number
  • boolean
  • bigint
  • symbol
  • undefined
  • object
  • function

TypeScriptはこの返り値を見て、変数の型を絞り込んでくれます。プリミティブ型の判別に最も使う型ガードです。

function describe(value: string | number | boolean): string {
  if (typeof value === "string") {
    return `文字列: ${value.toUpperCase()}`;
  }
  if (typeof value === "number") {
    return `数値: ${value.toFixed(2)}`;
  }
  // ここに来たら value は boolean
  return `真偽値: ${value ? "true" : "false"}`;
}

// typeof の落とし穴: null は "object" を返す
const maybeNull: object | null = null;
console.log(typeof maybeNull); // "object"
// 配列も "object" になる
console.log(typeof [1, 2, 3]); // "object"

このコードのポイントは次の通りです。

  • string | number | booleanのような複数プリミティブのユニオン型は、typeofの連続分岐できれいに絞れる
  • typeof null"object"を返すというのが、JavaScript由来の有名な罠
  • 配列も"object"になるため、配列かどうかはArray.isArray(value)で判定する

nullを含むユニオン型を絞り込みたいときは、typeofだけでは不十分です。x === nullでの等価比較や、x !== nullとの組み合わせを使うほうが安全です。

なかむぅ
なかむぅ
typeof null === "object"が腑に落ちないという方は、nullundefinedの違いを整理した記事も参考になります。
【TypeScript】nullとundefinedの違いは?使い分けと安全に扱う方法
【TypeScript】nullとundefinedの違いは?使い分けと安全に扱う方法TypeScriptのnullとundefinedの違いを初心者向けに解説。使い分け方や型の書き方、エラーを防ぐ安全な扱い方までわかりやすく学べます。 ...

instanceofで判定する|クラスインスタンスの絞り込み

instanceofは、ある値が特定のクラスのインスタンスかを判定するJavaScriptの演算子です。ErrorDateのような組み込みクラス、そして自作クラスの判別に向いています。

たとえば、関数の戻り値がErrorオブジェクトか文字列メッセージのどちらかという設計でよく使います。

function logResult(result: Error | string): void {
  if (result instanceof Error) {
    // result: Error
    console.error("エラー発生:", result.message, result.stack);
    return;
  }
  // result: string
  console.log("成功:", result);
}

// Date クラスを判別する例
function toISOString(value: Date | string): string {
  if (value instanceof Date) {
    return value.toISOString(); // value: Date
  }
  return value; // value: string
}

このコードのポイントは次の通りです。

  • instanceofの右辺にはクラス(コンストラクタ関数)を置く
  • 分岐内では、ErrorDateが持つメソッド(.message.stack.toISOStringなど)を安全に呼べる
  • instanceofはプロトタイプチェーンをたどるため、継承関係も正しく判定される

ここで注意したいのは、instanceofプレーンオブジェクトには使えないという点です。const user = { name: "Alice" }のようにクラスを介さず作ったオブジェクトは、特定のコンストラクタに紐づいていないため判別できません。プレーンオブジェクトの判別には、次に紹介するin演算子やユーザー定義型ガードを使います。

in演算子で判定する|プロパティの有無で絞り込む

in演算子は、オブジェクトが指定のプロパティを持っているかを判定します。interfacetypeで定義した構造的型を判別したいときに有効です。

「魚と鳥のどちらか」を表すユニオン型を例にしてみましょう。

type Fish = { kind: "fish"; swim: () => void };
type Bird = { kind: "bird"; fly: () => void };

function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    // animal: Fish
    animal.swim();
    return;
  }
  // animal: Bird
  animal.fly();
}

このコードのポイントは次の通りです。

  • "swim" in animalという形で、判別したいプロパティ名を文字列リテラルとして渡す
  • swimを持つ側がFish、持たない側がBirdとコンパイラが判断してくれる
  • クラスを使わないオブジェクト同士の判別に向く

ひとつだけ注意点として、in演算子はプロトタイプチェーン上のプロパティもtrueを返します。実務でよくあるプレーンなデータ構造の判別ならまず問題ありませんが、prototype汚染が絡む場面ではObject.hasOwn(animal, "swim")(ES2022で導入)のように自前で確認する必要が出てくる場合もあります。

古い実行環境を考慮する場合は、従来からあるObject.prototype.hasOwnProperty.call(animal, "swim")が等価な代替になります。

リテラル型の等価比較で判定する|タグ付きユニオンを絞り込む

最後の組み込み型ガードは、リテラル型の等価比較です。各バリアントに共通のプロパティ(判別子/discriminant)を持たせておき、その値を===で比較することで型を絞り込みます。

この設計はタグ付きユニオン(判別共用体)と呼ばれ、TypeScriptで状態を表すときの王道パターンです。switchとの相性が抜群で、never型を組み合わせれば網羅性チェックまで実現できます。

type Success<T> = { kind: "success"; data: T };
type Failure = { kind: "error"; message: string };
type Result<T> = Success<T> | Failure;

function handle(result: Result<number>): string {
  switch (result.kind) {
    case "success":
      // result: Success<number>
      return `成功: ${result.data}`;
    case "error":
      // result: Failure
      return `失敗: ${result.message}`;
    default: {
      // すべてのケースを処理しているなら、ここには絶対に到達しない
      // result の型はここで never になる
      const exhaustive: never = result;
      return exhaustive;
    }
  }
}

このコードのポイントは次の通りです。

  • すべてのバリアントが共通プロパティkindを持ち、その値("success""error")で型が判別される
  • switchの各case内では、その型に絞り込まれた状態でプロパティにアクセスできる
  • defaultconst exhaustive: never = resultとしておくと、新しいバリアントを追加したのにcaseの追加を忘れた瞬間に型エラーになる

この網羅性チェックは、後からバリアントが増えたときの「対応漏れ」を防いでくれます。タグ付きユニオンを使うなら、ぜひセットで覚えておきたいパターンです。

ユーザー定義型ガードとis演算子の書き方

組み込みの型ガード4種類でも、現実の判別を全部カバーするのは難しい場面があります。たとえばAPIからunknownで受け取ったレスポンスを、自前のUser型として扱いたいときなどです。

そんなときに使うのが、ユーザー定義型ガードです。戻り値の型に引数名 is 型という特別な書き方をした関数を作ると、その関数がtrueを返した分岐で、TypeScriptがその型として変数を扱ってくれます。

文法はとてもシンプルです。

function isFoo(x: unknown): x is Foo {
  // x が Foo であるかを実行時に検証し、真偽値を返す
}

戻り値のx is Foo型述語(type predicate)と呼ばれます。isは演算子ではなく、あくまで戻り値の型表記の一部である点に注意してください。

型述語(is演算子)の基本構文

まずは最小の型述語から書いてみましょう。引数としてunknownを受け取り、stringであることを検証する関数です。

function isString(x: unknown): x is string {
  return typeof x === "string";
}

// 使う側
function printLength(value: unknown): void {
  if (isString(value)) {
    // value: string に絞り込まれる
    console.log(value.length);
  }
}

このコードのポイントは次の通りです。

  • 関数本体は普通の真偽値を返す関数でしかない
  • 違いは戻り値型にx is stringと書いてあること。これで「trueを返したらこの引数はstring」とコンパイラに伝えている
  • 呼び出し側のif (isString(value))の中では、valuestringとして扱われる

ここで意識したいのは、isは演算子ではなく、戻り値の型表記の一部ということです。x is stringは型の世界の表現で、実行時には完全に消えます。実行時に動くのはあくまでreturn typeof x === "string"という普通のJavaScriptコードです。

オブジェクト構造を判別する型ガード関数

実務で型ガードを書くシーンは、ほとんどが「外部から来たオブジェクトを自前の型として扱いたい」ケースです。APIレスポンスがその代表例ですね。

{ id: number; name: string }というUser型を例に、ガード関数を書いてみましょう。

type User = {
  id: number;
  name: string;
};

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value
  );
}

// 使う側
async function fetchUser(): Promise<User> {
  const response = await fetch("/api/user");
  const data: unknown = await response.json();

  if (isUser(data)) {
    return data; // data: User
  }
  throw new Error("レスポンスがUser型ではありません");
}

このコードのポイントは次の通りです。

  • typeof value === "object"だけではnullも通ってしまうため、value !== nullを必ずセットで書く
  • in演算子でidname存在を確認している
  • 検証を通過した時点で、呼び出し側ではvalueを安全にUserとして使える

これは「最低限の形」です。実際にはidnumbernamestringであることまで確認しないと、形だけ揃った別物を通してしまう恐れがあります。プロパティの型まで含めた堅牢な書き方は、後半のアンチパターン解説のあとで段階的に組み立てます。

Array.prototype.filterと型述語の組み合わせ

ユーザー定義型ガードのもうひとつの定番が、Array.prototype.filterとの組み合わせです。配列からundefinednullを取り除きたいとき、コールバックの戻り値型に型述語を書くことで要素の型まで絞り込めます。

const values: (string | undefined)[] = ["a", undefined, "b", undefined, "c"];

// コールバックの戻り値型に型述語を書くと string[] に絞り込める
const narrowed = values.filter((x): x is string => x !== undefined);
// narrowed: string[]

このコードのポイントは次の通りです。

  • コールバックの戻り値型にx is stringと書くと、TypeScriptが「残った要素はstring」と判断する
  • これで配列の型が(string | undefined)[]からstring[]に変わる
  • 自前のガード関数を渡すときも同じ理屈で動く(例:values.filter(isString)

TypeScript 5.5以降は、x !== undefinedx != nullのような単純な検証であれば、型述語を書かなくてもコンパイラが自動で推論してくれるようになりました(Inferred Type Predicates)。たとえばvalues.filter((x) => x !== undefined)だけでも結果がstring[]になります。ただし、複雑な条件や自作のガード関数を経由する場合は、依然としてx is stringの明示が必要なケースもあります。バージョン差で挙動が変わる箇所のため、迷ったら型述語を明示しておくと安全です。

undefinednullを除いた型は標準ユーティリティ型のNonNullable<T>でも表現できます。

typeof・instanceof・in・ユーザー定義型ガードの使い分け

ここまで紹介した5パターンを、どんなときにどれを使うかでまとめておきます。実務では次の判断基準で十分です。

判定したい対象 使う型ガード
プリミティブ型(string/number/boolean等) typeof
クラスのインスタンス(Error/Date/自作クラス) instanceof
プロパティ有無で見分けるオブジェクト in演算子
判別子(kind等)を持つタグ付きユニオン リテラル等価比較
unknownから自前の型を組み立てる ユーザー定義型ガード(is

同じ「外部から来たデータを判別する」という場面で、対象別に書き分けるとどうなるかを並べてみましょう。

// 1. プリミティブ:API パラメータが string か number か
function parseId(input: string | number): number {
  if (typeof input === "string") {
    return Number(input);
  }
  return input;
}

// 2. クラスインスタンス:catch した値が Error かどうか
function logError(error: unknown): void {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("不明なエラー", error);
  }
}

// 3. タグ付きユニオン:APIレスポンスの状態
type ApiResponse = { kind: "ok"; data: string } | { kind: "ng"; reason: string };
function show(res: ApiResponse): string {
  return res.kind === "ok" ? res.data : res.reason;
}

// 4. 構造体:unknown から自前の型へ
type Point = { x: number; y: number };
function isPoint(v: unknown): v is Point {
  return (
    typeof v === "object" &&
    v !== null &&
    typeof (v as { x: unknown }).x === "number" &&
    typeof (v as { y: unknown }).y === "number"
  );
}

このコードのポイントは次の通りです。

  • 判別対象が決まれば、使うべき型ガードはほぼ自動的に決まる
  • 「とりあえずas」ではなく、まずは判別対象を言語化することから始める
  • 複数の型ガードを組み合わせる場面(タグ付きユニオン+ユーザー定義など)も実務では普通にある

迷ったときは、「判別したい違いは何か?」を一度書き出してみてください。プリミティブの種類なのか、クラスなのか、プロパティの有無なのか、それともプロパティの値なのか。これが決まれば、対応する型ガードが見えてきます。

型ガード関数を書くときのアンチパターン

ユーザー定義型ガードは便利ですが、書く側になると一気に責任が重くなります。なぜなら、TypeScriptは型述語を信じきるからです。

ここで注意したいのは、型述語と実装の整合性はコンパイラが検証してくれないという点です。v is Userと書いてあれば、実装が何を返していてもコンパイラはそれを信用します。これをunsoundness(健全でないこと)と呼びます。型ガード関数の中身を間違えると、それがそのまま実行時バグに直結します。

型述語が嘘をついてもTypeScriptは止めない

極端な例を見てみましょう。

type User = { id: number; name: string };

// 嘘つきな型ガード関数(コンパイラは通る)
function isUser(value: unknown): value is User {
  return true; // 何も検査せずに常に true を返す
}

const data: unknown = "これは文字列であってUserではない";

if (isUser(data)) {
  // ここでは data: User と扱われる
  console.log(data.name.toUpperCase()); // 実行時エラー!
}

このコードのポイントは次の通りです。

  • isUserは実際の値を一切検査せずtrueだけ返している
  • それでも型述語value is Userがあるおかげで、コンパイラは何のエラーも出さない
  • 実行時、dataは文字列なので.nameundefined.toUpperCase()で例外が飛ぶ

これは極端な例ですが、現場でもっと多いのはnullチェック忘れ」や「配列の判定でtypeofを使ってしまう」といった中途半端なミスです。たとえば次のようなケースです。

// よくある間違い: null チェック漏れ
function isUserBad(v: unknown): v is User {
  // typeof null === "object" なので、v が null でも true 寄りの結果になりうる
  return typeof v === "object" && "id" in (v as object);
}

// よくある間違い: 配列を typeof で判定
function isStringArrayBad(v: unknown): v is string[] {
  // 配列も typeof "object" になるため、ただのオブジェクトでも通ってしまう
  return typeof v === "object";
}

null"object"扱い、配列も"object"扱い。これらの罠を踏むと、ガード関数が本来弾くべき値を素通りさせます。さらに型述語のおかげで実行時バグまでコンパイラが気付けません。

anyにキャストすればよさそう」と思いたくなる場面もありますが、それは型システムを捨てる選択であってガードではありません。

堅牢に書くためのチェックリスト

実務で安全な型ガード関数を書くには、段階的にチェックを積み上げるのが基本です。順序は次の通り。

  • nullundefinedを弾くv !== null && v !== undefined、またはtypeof v === "object" && v !== null
  • オブジェクトかどうか確認:プリミティブを弾く
  • 配列ならArray.isArrayを使う:配列はtypeofで判別できない
  • プロパティの存在を確認"id" in vなど
  • プロパティの型まで確認typeof v.id === "number"など

この順序で書いた、堅牢なisUserの最終形が次のコードです。

type User = { id: number; name: string };

function isUser(value: unknown): value is User {
  // 1. null / undefined / プリミティブを弾く
  if (typeof value !== "object" || value === null) {
    return false;
  }
  // 2. 配列を弾く(User として扱いたくないため)
  if (Array.isArray(value)) {
    return false;
  }
  // 3. 必要なプロパティの存在と型を確認
  const obj = value as Record<string, unknown>;
  if (typeof obj.id !== "number") {
    return false;
  }
  if (typeof obj.name !== "string") {
    return false;
  }
  return true;
}

このコードのポイントは次の通りです。

  • 早期リターンで段階的に弾いていくので、読み手にも検証ロジックが追いやすい
  • 配列の扱いを意識的に決めている(ここでは「Userとして扱わない」と明示)
  • 内部で使うas Record<string, unknown>は、unknownから安全にプロパティアクセスするための限定的な使い方

検証ロジックが複雑になってきたら、スキーマ検証ライブラリに任せることも検討してみてください。手書きの型ガード関数は数が増えるとメンテナンスが大変になります。

assertsキーワード(アサーション関数)への発展

型ガードと似たもうひとつの仕組みに、アサーション関数があります。戻り値型にasserts x is Tと書いた関数のことで、「ここを通過したら以降はTとして扱う/そうでなければ例外を投げる」という挙動になります。

型ガードとアサーション関数の違いを比べてみましょう。

観点 型ガード関数 アサーション関数
戻り値型 x is T asserts x is T
戻り値 true / false(真偽値) なし(void
検証失敗時の挙動 falseを返す 例外を投げる(throw
呼び出し側 if (isFoo(x))で分岐 関数呼び出し後ずっとT扱い

実装例も見ておきましょう。

function assertIsString(x: unknown): asserts x is string {
  if (typeof x !== "string") {
    throw new Error(`string が期待されましたが、${typeof x} でした`);
  }
}

// 使う側
function shout(value: unknown): string {
  assertIsString(value);
  // この行以降では value: string として扱われる
  return value.toUpperCase();
}

このコードのポイントは次の通りです。

  • asserts x is stringは「この関数が例外を投げずに返ってきたらxstringである」とコンパイラに伝える
  • 呼び出し後は分岐なしでそのままstringとして使える
  • 検証失敗時は呼び出し側ではなく関数の中で例外が投げられる

アサーション関数は、「検査に通らなかったら処理自体を止めたい」場面に向いています。型ガード関数のように分岐を書かなくていいぶん、コードがスッキリすることがあります。

よくある質問

TypeScriptの型ガードと型アサーション(as)の違いは?

型ガードは「実行時に値を検査して型を絞り込む」仕組みで、asは「実行時のチェックなしにコンパイラに型を伝える」だけの宣言です。asは実行時に何もチェックしないため、間違った型を指定しても気付けません。基本は型ガードを優先し、asは「コンパイラを納得させたいだけの軽い表明」として最後の手段に留めるのが安全な書き方です。

typeofnull"object"になるのはなぜ?

これはJavaScriptの仕様上の歴史的な経緯によるものです。nullは本来「値が存在しない」ことを表すプリミティブですが、typeof null"object"を返してしまいます。後方互換性のためにずっと修正されていません。nullを判定したいときはx === nullで直接比較するか、typeof x === "object" && x !== nullの組み合わせで対処してください。

instanceofがうまく動かないのはどんなとき?

主な落とし穴は3つあります。1つ目はプレーンオブジェクト{ ... }リテラル由来のオブジェクト)には使えないこと。2つ目はクラスの継承関係で、子クラスのインスタンスは親クラスのinstanceofもtrueになる点。3つ目はiframeなど異なるrealmにまたがる場合で、同じArrayという名前でもrealmが違うとコンストラクタが別物になり、判定が意図通りにならないことがあります。

ユーザー定義型ガード(x is T)はどんな関数なら書ける?

戻り値が真偽値(boolean)の関数なら、戻り値型にx is Tを書いてユーザー定義型ガードにできます。ただし、実装と型述語の整合性はコンパイラが検証してくれません。v is Userと書いてあっても、内部でreturn trueしているだけでもエラーは出ません。検査ロジックが正しいかどうかは書き手の責任になります。複雑な検証はスキーマ検証ライブラリに委ねるのも有力な選択肢です。

is演算子とasはどちらを使うべき?

実行時の値を実際に検査する必要があるなら、ユーザー定義型ガード(is)を使うのが原則です。asはあくまでコンパイラに「この型として扱って」と伝えるだけで、実行時には何もしません。asを使うのは、「絶対に型は合っていると確信できるが、コンパイラがそこまで推論しきれない」というごく限定的な場面に絞るのが安全です。

タグ付きユニオンとswitchの網羅性チェックはどうやる?

default分岐でconst _: never = valueのように代入する書き方が定番です。すべてのケースを処理していればdefaultに到達することはなく、valueの型はneverになります。ここで新しいバリアントが増えると、まだ処理していないケースが残るためvalueの型がneverではなくなり、代入で型エラーが発生します。これによって「ケース追加時に対応漏れがある」とコンパイル時に気付けます。

型ガードとasserts関数はどう使い分ける?

「真偽値で分岐して、両方の結果に対応したい」場合は型ガード関数を使います。「検証に失敗したらそこで処理を止めたい(例外を投げたい)」場合はasserts関数を使います。前者は呼び出し側でif分岐が必要ですが、後者は関数を呼んだ後はそのまま絞り込んだ型として使える違いがあります。

【付録】さらに学びを深めるためのリソース


さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。

おすすめの書籍

ゼロからわかる TypeScript入門


技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。

変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。

プロを目指す人のためのTypeScript入門


技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。

オンラインで参照できる公式ドキュメント

TypeScript公式ハンドブック


https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。

TypeScript Deep Dive


https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。

TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。


まとめ|型ガードを使い分けて型安全なコードへ

TypeScriptの型ガードは、ユニオン型やunknownのような「広い型」を、安全に「狭い型」として扱えるようにする仕組みです。組み込みの4種類と、自分で書くユーザー定義型ガードの合計5パターンを覚えれば、実務の型チェックの大半に対応できます。

最後に判別対象別の使い分けを早見表で振り返っておきましょう。

判別したい対象 使う型ガード
プリミティブ型 typeof
クラスのインスタンス instanceof
プロパティ有無で見分けるオブジェクト in演算子
判別子つきのタグ付きユニオン リテラル等価比較
unknownから自前の型を組み立てる ユーザー定義型ガード(is

この記事で押さえたポイントを振り返ります。

  • 型ガードは「実行時の値検査」と「コンパイル時の型絞り込み」の二面性を持つ仕組み
  • 組み込み4種類(typeofinstanceofin/リテラル等価)は、判別対象別に使い分ける
  • ユーザー定義型ガード(x is T)は強力だが、型述語が嘘をついてもコンパイラは止めないunsoundnessに注意
  • 堅牢に書くには「nullチェック→typeof確認→プロパティ確認→値の型確認」の順に積み上げる
  • 検証失敗時に処理を止めたいならasserts関数、分岐で続行したいなら型ガード関数

asに頼って型エラーを押し切るより、型ガードを使い分けて「コンパイラに正しく型を理解させる」書き方を意識すると、コードはぐっと安全になります。最初は4種類の使い分けとisの文法だけでも十分です。少しずつ引き出しを増やしていきましょう。

※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。