型アサーション

TypeScript でコンパイラに型情報を明示的に伝える構文で、誤用するとランタイムエラーの原因になる

TypeScript型安全

型アサーションとは

型アサーション (Type Assertion) は、TypeScriptコンパイラに「この値はこの型である」と明示的に伝える構文である。as キーワードで記述する。コンパイラの型推論を上書きするため、誤用するとランタイムエラーの原因になる。

他言語の型キャストと決定的に違うのは、値の変換が起きないことだ。公式ハンドブックは、型アサーションは型注釈と同じくコンパイラによって取り除かれ実行時の挙動に影響しない、アサーションが誤っていても例外も null も生まれない、と明記している。生成後の JavaScript から as の記述は消える。型アサーションは値を直す操作ではなく、コンパイラの見え方だけを差し替える操作である。

書ける変換には制限がある

as は何でも通るわけではない。TypeScript が許すのは、元の型のより具体的な版か、より一般的な版への変換だけである。重なりのない型へ書くと不可能な変換として弾かれる。

const n = 'hello' as number;
// error TS2352: Conversion of type 'string' to type 'number' may be a mistake
// because neither type sufficiently overlaps with the other.
// If this was intentional, convert the expression to 'unknown' first.

この規則を知っていると、as で起こる事故の形が見えてくる。コンパイラが弾くのは型の階層として筋が通らない変換だけで、筋は通っているが実際の値が伴っていないケースは全部通る。通ったという事実は安全性を何も保証しない。

基本的な使い方

// DOM API は具体的な要素型を知らないので戻り値は HTMLElement | null
const element = document.getElementById('app') as HTMLDivElement;

// 外部から来た値の型を呼び出し側で決める
const data = JSON.parse(body) as User;

// EventTarget | null を入力要素として扱う
document.addEventListener('input', (e) => {
  const target = e.target as HTMLInputElement;
  console.log(target.value);
});

2 つ目の例には落とし穴がある。JSON.parse の戻り値は unknown ではなく any である (標準ライブラリ lib.es5.d.ts の宣言が parse(text: string, reviver?): any)。any はどの型にも代入できるので、as User を書かなくてもコンパイルは通り、書いても検査は 1 つも増えない。ここでの as User は「User のはずだ」という読み手向けの注記でしかなく、中身を確かめていない事実は変わらない。fetchresponse.json() も同じく Promise<any> である。

検証を強制したいなら、any を早い段階で unknown に落とす薄い包みを 1 つ置くとよい。unknown は何にも代入できないため、先へ進むには型ガードかバリデーションを通すしかなくなる。

const parseJson = (text: string): unknown => JSON.parse(text);

型アサーションの危険性

// ❌ 危険: 実際の値と型が一致しない
const user = { name: 'Alice' } as User; // User に email が必須でも通る
console.log(user.email.toLowerCase()); // TypeError: undefined の toLowerCase は呼べない

// ❌ 危険: API レスポンスを検証なしでアサーション
const response = await fetch('/api/users/123');
const fetched = await response.json() as User; // レスポンスが User 型とは限らない

1 つ目がなぜ弾かれないのかは前節の規則で説明できる。{ name: string } から見た User はプロパティが 1 つ増えたより具体的な型なので、コンパイラは「あり得る変換」と判断する。判断するのは型の形だけで、email が実在するかは調べない。型アサーションはコンパイラの型チェックを迂回する構文であり、実際の値が型と一致しなくてもコンパイルエラーは出ない。

型アサーションの代替手法

型ガード (推奨)

// ✅ 型ガード: 実行時に型を検証
function isUser(value: unknown): value is User {
  return typeof value === 'object' && value !== null
    && 'name' in value && 'email' in value;
}

const data: unknown = parseJson(body);
if (isUser(data)) {
  console.log(data.email); // プロパティの存在は検証済み
}

型ガードにも書き方次第の穴がある。in はプロパティの有無しか見ないので、{ name: 'Alice', email: 42 } はこの isUser を通過し、data.email.toLowerCase() は実行時に落ちる。戻り値の型述語 value is User は「この関数が true を返したら User と扱う」という宣言であって、関数の中身が正しいかはコンパイラが検査しない。プロパティごとに typeof まで確認するか、次のスキーマ検証に任せるのが実務的である。

Zod によるバリデーション (推奨)

import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  email: z.email(),
});

// ✅ バリデーション + 型推論
const user = UserSchema.parse(parseJson(body));
// user は { name: string; email: string } 型に推論される
// バリデーション失敗時は ZodError がスロー

スキーマ検証は型ガードの手書き部分を丸ごと引き受ける。検証を通った値の型がスキーマから導出されるので、型と検証の二重管理も起きない。2026 年 8 月時点の Zod 4 系ではメール形式などの文字列書式が最上位の関数へ移り、z.string().email() は動作はするが非推奨になっている。古い記事のコード例をそのまま持ち込まないよう注意したい。

型アサーションが許容されるケース

ケース理由補足
DOM 要素の取得getElementById は文書の中身を知らないため戻り値が HTMLElement | null に固定されている具体的な要素型に依存する処理なら instanceof HTMLInputElement で確かめる方が安全
テストコード必要な数個のプロパティだけを持つモックを型に合わせる本番コードに同じ書き方が漏れないようテスト用の生成関数に閉じ込める
外部ライブラリの型定義が不正確型定義のバグを回避する回避箇所にコメントで理由と追跡先を残す
リテラルの型を保ちたい型を差し替えたいのではなく制約だけ確かめたいas ではなく後述の satisfies を先に検討する

Non-null Assertion (!)

// ❌ Non-null Assertion: null チェックをスキップ
const risky = document.getElementById('app')!; // null なら実行時エラー

// ✅ 明示的な null チェック
const element = document.getElementById('app');
if (!element) throw new Error('Element not found');
// この行以降、element は HTMLElement 型

!as は危険の種類が違う。as は型全体を別の型に差し替えるので誤りの幅が大きい。一方 ! が消すのは nullundefined だけだが、記号 1 文字で書けるため入り込みやすく、後の改修で null になり得る経路が増えても警告が出ないという厄介さがある。どちらが上かではなく、! は「今この瞬間は絶対に存在する」と書き手が保証した印であり、その保証が将来も成り立つ根拠があるかで判断する。ESLint@typescript-eslint/no-non-null-assertion ルールで一律禁止し、必要な箇所だけ理由を添えて例外にする運用が扱いやすい。

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

// ❌ 型の階層を無視して任意の型に変換できる
const num = 'hello' as unknown as number;

unknown を経由すると前節の「具体化か一般化のみ」という規則を外せる。無関係な型どうしを繋げられるので、静的な手がかりは何も残らない。

とはいえ公式ハンドブックはこの二段アサーション自体を禁じてはおらず、規則が保守的すぎて妥当な変換まで弾く場合の逃げ道として any または unknown を挟む形を示している。実際に必要になるのは、幽霊型 (Phantom Type) で同じ実体のまま型パラメータだけを次の状態へ進めるときや、テストでモックを型に合わせるときである。判断の線は「無関係な型を繋いでいないか」に置く。繋いでいるなら設計かバリデーションの側を直す。

satisfies との使い分け

TypeScript 4.9 で入った satisfies は、型を差し替えずに制約を満たしているかだけを検査する。アサーションで潰れがちな推論結果を保てるので、設定オブジェクトやテーマ定義のような「型に適合させたいがリテラルの情報も残したい」場面では as より適する。

type Palette = Record<'primary' | 'danger', `#${string}`>;

// ❌ as: 制約違反を見逃す
const bad = { primary: '#fff', danger: 'red' } as unknown as Palette;

// ✅ satisfies: 制約は検査し、推論は保つ
const good = { primary: '#fff', danger: '#f00' } satisfies Palette;
const primary: '#fff' = good.primary; // リテラル型が残る

satisfies 側に danger: 'red' と書けば、# 始まりのテンプレートリテラル型に代入できないとしてコンパイルエラーになる。as Palette で書いた場合は型が Palette に固定されるため、good.primary の型も # 始まりの文字列全体まで広がり、リテラルの情報が失われる。

as const は別の構文

同じ as を使うが、as const (const アサーション) は型を差し替える構文ではない。リテラルの型が広がるのを止め、オブジェクトや配列を読み取り専用として扱わせるための指定である。

const arr = ['a', 'b'] as const; // readonly ['a', 'b']
const first: 'a' = arr[0];

as HTMLDivElement の系統と混同すると、危険な構文だと誤解して必要な場所で使わなくなる。名前が似ているだけの別物として覚えておく。

判断基準

型アサーションを書きたくなったら、次の順で考えると迷いにくい。

  1. 値の出どころが外部 (通信・ファイル・ユーザー入力) なら、アサーションではなくスキーマ検証を通す。境界を 1 か所に集めておけば、内側は型を信じて書ける。
  2. 出どころが自分のコードなら、型定義か関数のシグネチャを直せないかを先に見る。アサーションが必要になるのは、型の設計がその流れを表現できていない兆候であることが多い。
  3. 制約を確かめたいだけなら satisfies、実行時に確かめたいなら型ガードか instanceof を使う。
  4. それでも as が必要なら、何を根拠にその型だと言えるのかをコメントに残す。将来その根拠が崩れたときに気づけるかどうかが、事故になるかならないかの分かれ目である。

この記事は役に立ちましたか?

関連用語

関連する記事