型安全性

型システムが禁じている操作をプログラムが実行しないことを処理系が保証する性質。静的/動的と強い/弱いの 2 軸、健全性、型検査が破れる経路を解説

型システムTypeScript

型安全性とは

型安全性 (Type Safety) は、型システムが禁じている操作をプログラムが実行してしまわないことを、言語処理系が保証する性質である。保証を果たす時点は言語によって違う。静的型付けの言語は翻訳時に不正な操作を弾き、動的型付けの言語は実行時に値の型を見て例外を投げる。つまり「翻訳時に検出すること」自体が型安全性の定義ではない - 実行時に必ず型エラーとして止まる言語も型安全の側にある。

逆に、静的型付けでも型安全とは言えない言語がある。任意のポインタキャストで型検査を迂回できる C は、翻訳時に型を見るものの「型が付いたなら実行時もその型のとおりに振る舞う」保証を持たない。型を書く量ではなく、型の約束が実行時まで守られるかどうかが分かれ目である。

暗黙変換が生む不具合

動的型付けの JavaScript では + が数値と文字列の組み合わせを受け付けるため、引数の型違いが例外にならず結果の型だけが変わる。手元の Node.js (v26) で 1 + '2' を評価すると文字列 "12" が返り、エラーは発生しない。呼び出し側は数値を期待しているので、破綻はこの行より後ろの離れた場所で表面化する。

// ❌ JavaScript (動的型付け・暗黙変換あり): エラーにならず結果の型が変わる
function add(a, b) { return a + b; }
add(1, '2'); // "12" (文字列結合、意図しない挙動)

// ✅ TypeScript (型安全): コンパイル時にエラー
function add(a: number, b: number): number { return a + b; }
add(1, '2'); // ❌ コンパイルエラー: string は number に代入できない

混同されやすい 2 つの軸

型付けの議論では、性質の異なる 2 つの軸が 1 本の「強さ」にまとめられてしまいがちである。両者は独立しており、組み合わせは 4 通りすべて実在する。

軸何を決めるか例
静的 / 動的型検査を翻訳時に行うか実行時に行うか静的 = TypeScript, Rust, Java / 動的 = JavaScript, Python
強い / 弱い型の違う値の暗黙変換をどこまで許すか強い = Python, Rust / 弱い = JavaScript, PHP

Python は動的型付けだが暗黙変換には厳しく、'1' + 2 は TypeError で停止する (手元の Python 3 で確認)。JavaScript は同じ動的型付けでありながら 1 + '2' を黙って文字列にする。動的型付けを「型なし」と呼ぶのも正確ではない - JavaScript は値に型を持ち、実行時にそれを見て加算と連結を切り替えている。

コンパイラーの strict 系設定も、この 2 軸とは別の話である。設定が変えるのは同じ型システムでどこまで検査を要求するかであって、言語が弱い型付けになるわけではない。厳しくするほど守られる範囲は広がるが、後述する健全性の限界はどの設定でも残る。

健全性 (soundness) と工学的な妥協

型システムの理論では、型検査を通ったプログラムが実行時に型エラーを起こさないことを証明できるとき、その型システムを健全 (sound) と呼ぶ。健全性が保証するのは型の約束が破られないことだけで、「バグが出ない」ことではない。仕様の取り違えや計算の誤りは、型が完璧でもそのまま通る。

実務の言語は、この健全性を意図的に手放すことがある。TypeScript の公式な設計方針は非目標の一覧で、健全あるいは証明可能に正しい型システムを適用することを目指さず、正しさと生産性の釣り合いを取ると明言している (2026 年 8 月時点の記述)。既存の JavaScript 資産へ後から型を載せるという出発点上、既存コードを大量に不合格にする健全性よりも、既存の書き方を通せることを選んだ結果である。言語側の事情は TypeScript で扱う。

型検査は静的解析の一種であり、実行前の情報だけで判定する以上、判定は必ず近似になる (静的解析)。検査を通ったことは「型の約束の範囲では破綻しない」という限定された主張であり、それ以上を読み取ってはいけない。

型安全性が破れる 3 つの経路

型を書いているのに実行時に壊れる場合、原因はほぼ次の 3 つに絞られる。

  • any: 代入 / 呼び出し / プロパティアクセスの検査がその値について止まる。any の値を number の変数へ入れても検査は通り、実際に文字列が入っていれば数値向けメソッドの呼び出しで初めて壊れる。any は戻り値や引数を通って伝播するため、1 箇所の妥協が呼び出し先まで穴を広げる。
  • 型アサーション (as): 検査ではなく開発者による断定である。断定が誤っていてもコンパイラーは何も言わない。後述の Branded Type も生成時はこの断定に頼るので、生成箇所は数箇所に閉じ込め、そこだけ実行時の検証を伴わせる。
  • 外部との境界: API 応答 / JSON.parse / ブラウザーの保存領域から来る値は、宣言した型のとおりとは限らない。型は実行時のコードに残らないため、境界では実行時の検証を書く以外に守る手段がない。

穴を塞ぐ側の道具が unknown である。何でも受け取れるが、型を絞り込むまで一切使えないので、any を unknown に置き換えると「検証していない場所」がコンパイルエラーとして地図に現れる。

null と範囲外を型に出す

strict を有効にすると、null や undefined になり得る経路が型に現れ、確認を省いた参照が翻訳時に止まる。配列の範囲外アクセスは別枠で、noUncheckedIndexedAccess を明示的に有効にしたときだけ undefined が型に加わる - この設定は strict に含まれない (手元の TypeScript コンパイラー 6.0.3 で、strict のみでは範囲外アクセスがエラーにならないことを確認)。

// strictNullChecks: null/undefined を型で管理
function getUser(id: string): User | null { ... }
const user = getUser('123');
user.name; // ❌ コンパイルエラー: null の可能性
if (user) user.name; // ✅ null チェック後は安全

// noUncheckedIndexedAccess: 配列アクセスの安全性
const arr = [1, 2, 3];
const x: number = arr[10]; // ❌ number | undefined

取り得る値を型で狭める

string で受け取ると、綴りの誤りは実行するまで露見せず、条件分岐のどれにも当たらない値が静かに流れていく。取り得る値が有限なら、ユニオン型で候補を並べるだけで検査対象になり、綴りの誤りは翻訳時に止まる。状態を表す文字列や区分値はこの形が既定と考えてよい。

// ❌ string で何でも受け入れる
function setStatus(status: string) { ... }
setStatus('actve'); // タイポに気づかない

// ✅ ユニオン型で制限
type Status = 'active' | 'inactive' | 'suspended';
function setStatus(status: Status) { ... }
setStatus('actve'); // ❌ コンパイルエラー

同じ表現の別物を区別する

利用者 ID と注文 ID がどちらも string だと、引数の入れ替えは型検査を通ってしまう。そこで型の上だけの目印を交差型で付け、互換性を意図的に壊す手法が Branded Type である。目印は型の世界にしか存在せず、実行時の値は素の文字列のままなので、生成時の断定が正しいことは呼び出し側が担保する必要がある。

type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

function getUser(id: UserId): User { ... }
function getOrder(id: OrderId): Order { ... }

const userId = '123' as UserId;
const orderId = '456' as OrderId;
getUser(orderId); // ❌ コンパイルエラー: OrderId は UserId に代入できない

型安全性が返してくるもの

型が守るのは実行時の破綻だけではない。関数の期待する形が宣言として残るため、補完と参照検索が効き、名前や構造を変えたときに影響範囲がエラーの一覧として出てくる。文章のドキュメントと違って、型は古くなれば検査に落ちるので、更新されないまま嘘をつくことがない。

どこまで型で守るか

全域を健全にする努力は費用が急に増える一方、守れる範囲は境界の設計でほぼ決まる。判断は次の順で置くと迷いが減る。

  1. 外部から来る値は必ず実行時に検証する。ここを型宣言だけで済ませると、他の努力がまとめて無効になる。
  2. any は使う場所を決めて閉じ込める。伝播させない書き方 (戻り値を絞る、境界で unknown に受ける) を先に用意する。
  3. 断定 (as) は生成箇所に集約する。散らばった断定は、後から正しさを確認する手段が残らない。
  4. 取り得る値が有限なものは型で並べる。追加漏れが検査で落ちるようになり、分岐の網羅も確認できる。
共有:

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

関連用語

関連する記事