本文へスキップ
ウェブエンジニア問題集
第8章

型アサーションと型ガード — as・is・as constの使い分け

約22分
この章の目次開く

05章で学んだNarrowingは、TypeScriptが制御フローから自動的に型を絞り込む仕組みでした。この章では、開発者が明示的に型を指定する手法を扱います。

学習者学習者

コンパイラが「型が違う」って怒るとき、とりあえず as で黙らせちゃダメなの…?


型アサーション(as)

型アサーションは、TypeScriptの型推論結果を開発者が上書きする手段です。

構文: 値 as 型

// getElementById の戻り値は HTMLElement | null
const input = document.getElementById('my-input') as HTMLInputElement;
 
// HTMLInputElement として扱えるので、input 固有のプロパティが使える
console.log(input.value);
tsx

as を書いた瞬間、コンパイラは推論をやめて「開発者の申告」を信じます。データそのものは何も変わらず、型の見え方だけが変わる点がポイントです。

どういう時に使用するか

具体的な使用シーンは主に以下の3つです。

使用シーン説明
DOM要素を取得する時document.getElementById('my-input') などで要素を取得すると、TypeScriptはそれを単なる「一般的なHTML要素」としか認識しません。HTMLInputElement など具体型に断言することで、.value など要素固有のプロパティがエラーにならずに使えます。
外部データ(APIのレスポンスなど)を受け取る時JSON.parse() の結果など、外部から来たデータは中身がわからないため any や unknown 型になります。「このデータは User 型だ」と断言して、プログラム内で扱いやすくします。
イベントハンドラの引数を使う時クリック時などの event.target は抽象的な型になっているため、ボタンやリンクなど具体的なHTML要素の型に断言して操作します。

コードで書くと次のようになります。

// ① DOM要素を取得する時
const input = document.getElementById('email') as HTMLInputElement;
input.value = 'test@example.com';
 
// ② 外部データ(APIのレスポンスなど)を受け取る時
type User = { id: number; name: string };
const user = JSON.parse(jsonText) as User;
console.log(user.name);
 
// ③ イベントハンドラの引数を使う時
document.querySelector('button')?.addEventListener('click', (event) => {
  const target = event.target as HTMLButtonElement;
  target.disabled = true;
});
tsx

型アサーションは「コンパイラの安全装置をオフにして無理やり通す」行為です。そのため、本当にその型である確証がある時だけ使う(乱用しない)のが鉄則です。

型アノテーションと型アサーションの違い

型アノテーション と 型アサーション は、

  • 型アノテーション『注釈』

    変数や関数を定義する際に、「これにはこの型のデータが入る」と最初から明示的に指定するもの

  • 型アサーション『断定』

    コンパイラが自動で推論した型に対して、プログラマが「いいや、これはこの型として扱え」と強制的に上書きするものです

どちらも型を指定する役割を持ちますが、使われる目的やタイミングに違いがあります。

// 型アノテーション:定義時に「この型が入る」と宣言する
// 中身が User と合っていなければ、その場でコンパイルエラーになる
const user1: User = { id: 1, name: 'Tanaka' };
 
// 型アサーション:推論結果を「この型として扱え」と上書きする
// 中身が本当に User かどうかはチェックされない
const user2 = JSON.parse(jsonText) as User;
tsx
アノテーションはコンパイラのチェックを受ける「宣言」、アサーションはチェックを迂回する「断言」です。

型アサーションと型キャストの違い

型キャスト は別の型に変換する手段です。

一方、型アサーション はデータそのものは一切変更しません。TypeScriptのコンパイラに対する「見せ方(認識)」だけを変えます。

下記は型キャストの例

const n = 123; // nは数値(Number)
const s = String(n); // sは文字列(String)の "123" に変換される
tsx

非nullアサーション(!)

非nullアサーションは、変数の後ろに ! を付けることで「この値は null や undefined ではない」とコンパイラに断言する記法です。

値が null または undefined になりうる型(例:string | null)に対して、開発者が「ここでは絶対に値が存在する」と保証することで、null チェックなしでプロパティやメソッドにアクセスできるようになります。

function getLength(text: string | null) {
  // ① text は null の可能性があるため、そのまま .length にアクセスするとエラー
  // console.log(text.length); // ❌ 'text' is possibly 'null'.
 
  // ② ! を付けて「null ではない」と断言すると、エラーにならずアクセスできる
  console.log(text!.length);
}
tsx

よく使われるのは、DOM要素の取得結果のように「存在するはずだが、型の上では null の可能性が残る」ケースです。

// getElementById の戻り値は HTMLElement | null
const button = document.getElementById('submit')!;
// ! を付けることで HTMLElement として扱える
button.addEventListener('click', () => {});
tsx

そのため、可能な限り ! ではなく、より安全な代替手段を優先します。

手段例特徴
オプショナルチェーン(?.)text?.lengthnull / undefined なら処理を中断し undefined を返す
Null合体演算子(??)text ?? '初期値'null / undefined のときに既定値を使う
型ガード(if文によるチェック)if (text !== null) { ... }実行時に確認するため最も安全

! は「他に手段がなく、かつ値が存在することが確実に分かっている」場合の最後の手段として使うのが鉄則です。

型ガードで型安全を保証する

型ガード(Type Guard)とは、 プログラムの実行時にデータの中身をチェックすることで、TypeScriptの型を安全に絞り込む仕組みです。

function printValue(value: string | number) {
  // ① この時点では string か number か分からないため、文字列専用の value.length はエラーになる
 
  if (typeof value === 'string') {
    // ② 型ガードが働き、このブロック内では確実に「string(文字列)」として認識される!
    console.log(value.length); // エラーにならない
  } else {
    // ③ ここでは自動的に残りの「number(数値)」として認識される
    console.log(value * 10);
  }
}
tsx

typeof のほか、instanceof や in を使った基本の絞り込みは05章の型の絞り込み(Narrowing)で扱いました。ここでは、その絞り込みロジックを再利用可能な関数として切り出す方法を見ていきます。

ユーザー定義型ガード(is)

同じ判定ロジックをあちこちの if 文に書くのは冗長です。そこで判定処理を関数に切り出し、戻り値の型の位置に「型述語(type predicate)」を書くことで、呼び出し元でも型の絞り込みが効くようにしたものが、ユーザー定義型ガードです。

構文: function 関数名(引数名: 型): 引数名 is 絞り込み後の型

部分説明
引数名: 型チェックしたい値。外部データの検証では unknown で受けることが多い
引数名 is 絞り込み後の型戻り値の型の位置に書く型述語。この関数が true を返した場合、呼び出し元でその引数が指定した型に絞り込まれる

戻り値: boolean(true = その型である / false = 違う)

典型例は、APIレスポンスのような unknown なデータを検証してから使うパターンです。

type User = {
  id: number;
  name: string;
};
 
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof value.id === 'number' &&
    'name' in value &&
    typeof value.name === 'string'
  );
}
tsx

この isUser を通すと、if ブロックの中では unknown が User として扱えます。

async function fetchUser(): Promise<User | null> {
  const res = await fetch('/api/user');
  const data: unknown = await res.json();
 
  if (isUser(data)) {
    return data; // ✅ このブロック内では data は User 型
  }
  return null; // 検証に失敗したデータは通さない
}
tsx
学習者学習者

結局「これは User だ」って自分で申告してるんだから、as User と同じじゃないの…?

決定的な違いは、実行時チェックの有無です。as は中身を一切確認せずに型だけを上書きしますが、is を使った型ガードは「実行時に中身を検査し、合格したものだけを通す」仕組みです。as は「ノーチェックの自己申告」、is は「実行時検査に合格したことをコンパイラに伝える契約」です。

型ガードのイメージ
型ガードは中身を検査してから通す門番。asは顔パス

型述語は、配列の filter で「絞り込んだ結果の配列」を得たいときにも使えます。

const values = ['a', null, 'b', null]; // (string | null)[]
 
// 型述語をアロー関数の戻り値に書くと、結果が string[] になる
const strings = values.filter((v): v is string => v !== null);
tsx

なお、true / false を返す代わりに「不正なら throw する」形で切り出すアサーション関数(asserts)もあります。こちらは05章のアサーション関数パターンで扱いました。

ダブルアサーション(as unknown as T)の罠

学習者学習者

as any がダメなら as unknown as にすれば安全ですよね? AIに「anyを使わないで」って指示したら、こっちに書き換えてくれたんですけど……

結論から言うと、as unknown as T を書いている時点で型安全性は放棄しています。as any as T と本質的な危険度は大差ありません。

// どちらも「型チェックを黙らせる」行為
const a = value as any as TargetType;
const b = value as unknown as TargetType;
tsx

TypeScriptは通常、互換性のない型同士のアサーションをエラーにしてくれます。

const n: number = 42;
const s = n as string;
// ❌ 型 'number' から型 'string' への変換は、互いに十分に重複できないため
//    間違っている可能性があります。意図的にそうする場合は、
//    まず式を 'unknown' に変換してください。ts(2352)
tsx
学習者学習者

エラーメッセージ自体が「まず unknown に変換してください」と回避策を教えてくれるの、なんだか不思議です。これっておまじないですか?

なぜ unknown を挟むと通るのか

おまじないではなく、はっきりしたルールがあります。as は「どちらか一方向に代入できる型同士」でしか許可されません。これがエラーメッセージの言う「十分に重複」の正体です。number と string はどちらも互いに代入できないため、直接のアサーションは拒否されます。

一方 unknown は、あらゆる型を受け入れるトップ型です。そのため2段に分けると、それぞれが個別にルールを満たします。

const step1 = n as unknown; // ✅ 何でも unknown には代入できる
const step2 = step1 as string; // ✅ unknown は任意の型にアサーションできる
tsx

つまりダブルアサーションは、ルールを「経由」してすり抜けているだけです。そしてここが肝心なのですが、これはコンパイル時だけの話です。

const n = 42;
const s = n as unknown as string;
 
s.toUpperCase(); // 💥 TypeError: s.toUpperCase is not a function
tsx

実行時の値は数値のままなので、string のつもりでメソッドを呼べば落ちます。型注釈に嘘をついただけで、値そのものは1ビットも変わっていません。この number → string の例に限っていえば、エラーは正しく仕事をしていたので、消すのではなく従うのが正解です。値を本当に変えたいなら、この章の前半で見た型キャスト、つまり変換関数を使います。

String(n); // "42" — n.toString() や `${n}` でもよい
tsx

ダブルアサーションは、一度 any や unknown を経由することでこの安全装置を迂回します。unknown 経由のほうが @typescript-eslint/no-explicit-any ルールに引っかからないぶん行儀が良く見えますが、迂回していること自体は変わりません。

ダブルアサーションが出てきたら、まず設計を見直します。

状況代わりにやること
APIレスポンスの型が合わないジェネリクスで型パラメータを渡す / Zodなどでバリデーション
ライブラリの型定義が古い@types を更新する / 型定義を拡張(declaration merging)
テストでモックを作りたいPartial<T> や satisfies を活用する
本当にどうしようもないas unknown as T を使い、コメントで理由を残す

使ってよい場面 — 「実行時の値は正しいが、コンパイラには証明できない」とき

とはいえ、ダブルアサーションが常に悪というわけではありません。判断基準はシンプルです。

実行時の値判定
本当にその型になっている許容。なぜ安全といえるかをコメントで残す
その型になっていない修正すべきコードを隠しているだけなので不可

先ほどの number → string は後者なので論外でした。前者に当たる典型例は次の3つです。

  • テストのモック — 巨大なインターフェースの一部だけ作って渡したい
  • 型定義が実態とズレたライブラリの回避 — @types の更新が追いついていない
  • ジェネリクスの内部 — 実装者には正しいと分かるが、コンパイラが変換を追跡しきれない

いずれも「実行時の値は本当に正しい」のが前提です。頻度としては、普通のアプリケーションコードなら年に数回出るかどうかという程度で、日常的に書くものではありません。

ただし「モックだから常にダブルアサーションが要る」わけではない点に注意してください。

type User = { id: number; name: string; email: string };
 
// ✅ プロパティが足りないだけなら、1段の as で通る
const mockUser = { id: 1 } as User;
 
// ❌ プロパティの型が食い違うと ts(2352) になる
const mockRequest = { url: '/api', headers: { 'content-type': 'json' } } as Request;
tsx

理由は、さきほどのルール(片方向に代入できればOK)で説明できます。User は { id: 1 } の形に代入できるので前者は通り、headers が Headers 型と噛み合わない後者は両方向とも失敗します。unknown を足す前に、まず1段の as で足りないか試すのが正しい順番です。

先生先生

as unknown as に統一すること自体は悪くありません。lintルールと整合しますし、コードレビューで「ここは意図的にキャストしている」と分かりやすくなります。ただ、そもそもダブルアサーションが必要な設計自体を見直すほうが先、という順番が大事です。


constアサーション(as const)

as const は名前こそ「アサーション」ですが、これまでの as T とは性格が正反対です。型を偽るのではなく、推論される型をできるだけ狭く・固定的にするための機能で、安全に使えます。

まず、as const なしの推論結果を見てみます。

const config = {
  endpoint: '/api/users',
  method: 'GET',
};
// 推論結果: { endpoint: string; method: string }
tsx

method の値は 'GET' と書いたのに、型は string に広げられて(widening)推論されます。オブジェクトのプロパティは後から書き換えられるため、TypeScriptは安全側に倒して広い型を付けるのです。これが困る場面があります。

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
 
function request(url: string, method: HttpMethod) {
  /* ... */
}
 
request(config.endpoint, config.method);
// ❌ エラー:string は HttpMethod に代入できない
tsx

as const を付けると、推論結果がリテラル型のまま固定されます。

const config = {
  endpoint: '/api/users',
  method: 'GET',
} as const;
// { readonly endpoint: "/api/users"; readonly method: "GET" }
 
request(config.endpoint, config.method); // ✅ "GET" は HttpMethod に代入できる
tsx

as const の効果は次の3つです。

効果説明
リテラル型に固定'GET' が string に広げられず、"GET" 型のまま保たれる
すべて readonly になるプロパティへの再代入がコンパイルエラーになる(ネストしたオブジェクトも)
配列が読み取り専用タプルになる[10, 20] が number[] ではなく readonly [10, 20] になる

配列からユニオン型を導出する定番パターン

as const の実務での代表的な使いどころが、「選択肢の配列」と「その要素のユニオン型」を1つの定義から導出するパターンです。

const CATEGORIES = ['tech', 'design', 'business'] as const;
// 型: readonly ["tech", "design", "business"]
 
type Category = (typeof CATEGORIES)[number];
// 型: "tech" | "design" | "business"
 
// 実行時は配列として使える
CATEGORIES.map((c) => console.log(c));
 
// 型の世界ではユニオン型として使える
function setCategory(category: Category) {
  /* ... */
}
tsx
配列(実行時の値)とユニオン型(型の世界)を1つの定義から導出できるため、選択肢を追加するときの修正が1箇所で済みます。

型アサーション vs 型ガード — どちらを使うべきか

手法安全性使う場面
型ガード(typeof, in, ユーザー定義)高いランタイムで検証できる場合
型アサーション(as T)低い型が正しいと確信できる場合
ダブルアサーション(as unknown as T)最低テストやモックなどやむを得ない場面のみ
型アサーションと型ガード
迷ったら型ガード。asは「ここは自分が型チェッカーより詳しい」と確信できるときだけ

基本方針:まず型ガードで解決できないか考える。 as は「TypeScriptが推論できないが、自分には型が分かっている」場面の最終手段として使います。

ちゃんと使うためのポイント

  • 型ガードはランタイムで検証するので安全。型アサーションは自己申告なので危険
  • as any も as unknown as T も型安全性を放棄している点は同じ。ただし any は後続の式に伝播し、as unknown as T はその場で型が確定する
  • ts(2352) が出たら、unknown を挟む前に「1段の as で足りないか」「そもそも値の型が間違っていないか」を先に確認する
  • as const はアサーションではなくリテラル型への固定。安全に使える
  • ユーザー定義型ガード(is)はAPIレスポンスのバリデーションで特に有用。ただし検査ロジックの正しさは自分で保証する
  • TypeScript 5.5以降、単純な関数の型述語は自動推論される(特定の型へ絞る場合は明示的に is を書く)

次の章では、Partial や Pick など、既存の型から新しい型を作り出すユーティリティ型を学びます。この章の表に登場した「モックには Partial<T> を使う」の正体も、そこで分かります。

参考リンク

TypeScriptクイズに挑戦するこの章で学んだTypeScriptの知識を、4択クイズでアウトプットして定着させよう